import 'dart:math';

import 'package:currency_picker/currency_picker.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:prasule/api/category.dart';
import 'package:prasule/api/entry_data.dart';
import 'package:prasule/api/recurring_entry.dart';
import 'package:prasule/api/wallet_entry.dart';
import 'package:prasule/api/wallet_manager.dart';
import 'package:prasule/main.dart';
part 'wallet.g.dart';

Currency _currencyFromJson(Map<String, dynamic> data) =>
    Currency.from(json: data);

/// Represents a single wallet
///
/// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
@JsonSerializable()
class Wallet {
  /// A wallet stores [WalletSingleEntry]s categorized under [WalletCategory]s
  Wallet({
    required this.name,
    required this.currency,
    this.categories = const [],
    this.entries = const [],
    this.recurringEntries = const [],
    this.starterBalance = 0,
  });

  /// Connects the generated fromJson method
  factory Wallet.fromJson(Map<String, dynamic> json) => _$WalletFromJson(json);

  /// A list of all [RecurringWalletEntry]s
  final List<RecurringWalletEntry> recurringEntries;

  /// Name of the wallet
  final String name;

  /// A list of available categories
  final List<WalletCategory> categories;

  /// List of saved entries
  final List<WalletSingleEntry> entries;

  /// The starting balance of the wallet
  ///
  /// Used to calculate current balance
  double starterBalance;

  /// Selected currency
  @JsonKey(fromJson: _currencyFromJson)
  final Currency currency;

  /// Connects the generated toJson method
  Map<String, dynamic> toJson() => _$WalletToJson(this);

  /// Getter for the next unused unique number ID in the wallet's **entry** list
  int get nextId {
    var id = 1;
    while (entries.where((element) => element.id == id).isNotEmpty) {
      id++; // create unique ID
    }
    return id;
  }

  /// Getter for the next unused unique number ID in the wallet's **category**
  /// list
  int get nextCategoryId {
    var id = 0;
    while (categories.where((element) => element.id == id).isNotEmpty) {
      id++; // create unique ID
    }
    return id;
  }

  /// Handles adding recurring entries to the entry list
  void recurEntries() {
    final n = DateTime.now();
    for (final ent in recurringEntries) {
      var m = DateTime(
        (ent.recurType == RecurType.year)
            ? ent.lastRunDate.year + ent.repeatAfter
            : ent.lastRunDate.year,
        (ent.recurType == RecurType.month)
            ? ent.lastRunDate.month + ent.repeatAfter
            : ent.lastRunDate.month,
        (ent.recurType == RecurType.day)
            ? ent.lastRunDate.day + ent.repeatAfter
            : ent.lastRunDate.day,
      ); // create the date after which we should recur

      while (n.isAfter(
        m,
      )) {
        logger
          ..i("Adding recurring entry ${ent.data.name}")
          ..d("Current entry count: ${entries.length}");
        recurringEntries[recurringEntries.indexOf(ent)].lastRunDate =
            m; // update date on recurring entry

        final addedEntry = WalletSingleEntry(
          data: recurringEntries[recurringEntries.indexOf(ent)].data,
          type: recurringEntries[recurringEntries.indexOf(ent)].type,
          date: m,
          category: recurringEntries[recurringEntries.indexOf(ent)].category,
          id: nextId,
        );

        entries.add(addedEntry);

        m = DateTime(
          (ent.recurType == RecurType.year)
              ? recurringEntries[recurringEntries.indexOf(ent)]
                      .lastRunDate
                      .year +
                  ent.repeatAfter
              : recurringEntries[recurringEntries.indexOf(ent)]
                  .lastRunDate
                  .year,
          (ent.recurType == RecurType.month)
              ? recurringEntries[recurringEntries.indexOf(ent)]
                      .lastRunDate
                      .month +
                  ent.repeatAfter
              : recurringEntries[recurringEntries.indexOf(ent)]
                  .lastRunDate
                  .month,
          (ent.recurType == RecurType.day)
              ? recurringEntries[recurringEntries.indexOf(ent)]
                      .lastRunDate
                      .day +
                  ent.repeatAfter
              : recurringEntries[recurringEntries.indexOf(ent)].lastRunDate.day,
        ); // add the variable again to check if we aren't missing any entries
        logger.i(
          "Last recurred date is now on ${DateFormat.yMMMMd().format(m)} (${n.isAfter(m)})",
        );
      }
      WalletManager.saveWallet(this); // save wallet
    }
  }

  /// Removes the specified category.
  ///
  /// All [WalletSingleEntry]s will have their category reassigned
  /// to the default *No category*
  Future<void> removeCategory(WalletCategory category) async {
    // First remove the category from existing entries
    for (final entryToChange
        in entries.where((element) => element.category.id == category.id)) {
      entryToChange.category =
          categories.where((element) => element.id == 0).first;
    }
    // Remove the category
    categories.removeWhere((element) => element.id == category.id);
    // Save
    await WalletManager.saveWallet(this);
  }

  /// Returns the current balance
  ///
  /// Basically just takes *starterBalance* and adds all the entries to it
  double get currentBalance {
    var toAdd = 0.0;
    for (final e in entries) {
      toAdd += (e.type == EntryType.income) ? e.data.amount : -e.data.amount;
    }
    return starterBalance + toAdd;
  }

  /// Returns the amount that was made/lost during a month
  double calculateMonthStatus(int month, int year) {
    var f = 0.0;
    for (final e in entries.where(
      (element) => element.date.year == year && element.date.month == month,
    )) {
      f += (e.type == EntryType.income) ? e.data.amount : -e.data.amount;
    }
    return f;
  }

  /// Empty wallet used for placeholders
  static final Wallet empty = Wallet(
    name: "Empty",
    entries: [],
    recurringEntries: [],
    categories: [
      WalletCategory(
        name: "Default",
        id: 0,
        icon: IconData(
          Icons.payments.codePoint,
          fontFamily: 'MaterialIcons',
        ),
        color: Colors.white,
      ),
    ],
    currency: Currency.from(
      json: {
        "code": "USD",
        "name": "United States Dollar",
        "symbol": r"$",
        "flag": "USD",
        "decimal_digits": 2,
        "number": 840,
        "name_plural": "US dollars",
        "thousands_separator": ",",
        "decimal_separator": ".",
        "space_between_amount_and_symbol": false,
        "symbol_on_left": true,
      },
    ),
  );

  /// Creates test data used for debugging purposes
  Future<void> createTestEntries() async {
    entries.clear();
    recurringEntries.clear();
    final random = Random();
    for (var i = 0; i < 30; i++) {
      entries.add(
        WalletSingleEntry(
          data: EntryData(
            name: "Test Entry #${i + 1}",
            amount: random.nextInt(20000).toDouble(),
          ),
          type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income,
          date: DateTime(
            2023,
            random.nextInt(12) + 1,
            random.nextInt(28) + 1,
          ),
          category: categories[random.nextInt(categories.length)],
          id: nextId,
        ),
      );
    }

    logger.d(
      "Created ${entries.length} regular entries",
    );

    for (var i = 0; i < 3; i++) {
      final type = random.nextInt(3);
      recurringEntries.add(
        RecurringWalletEntry(
          data: EntryData(
            name: "Recurring Entry #${i + 1}",
            amount: random.nextInt(20000).toDouble(),
          ),
          type: (random.nextInt(3) > 0) ? EntryType.expense : EntryType.income,
          date: DateTime(
            2023,
            random.nextInt(12) + 1,
            random.nextInt(28) + 1,
          ),
          category: categories[random.nextInt(categories.length)],
          id: nextId,
          lastRunDate: DateTime.now().subtract(
            Duration(
              days: (type > 0) ? 3 : 3 * 31,
            ),
          ),
          recurType: (type > 0) ? RecurType.day : RecurType.month,
        ),
      );
    }

    logger.d(
      "Created ${recurringEntries.length} recurring entries",
    );

    // save and reload
    await WalletManager.saveWallet(this);
  }
}
