import 'dart:async';

import 'package:drift/drift.dart';

import '../Entities/drift_database.dart';
import '../Services/database_service.dart';

/// Funktionen zum interagieren mit der Datenbank für die Transaktionen
class TransactionRepository {
  final AppDatabase _db = DatabaseService().database;

  /// Fügt eine neue Transaktion zur Datenbank hinzu
  Future<Transaction?> add(final TransactionsCompanion transaction) async {
    final DateTime date = transaction.date.value!.add(
      transaction.date.value!.timeZoneOffset,
    );

    final TransactionsCompanion transactionToAdd = TransactionsCompanion(
      id: transaction.id,
      name: transaction.name,
      date: Value(date),
      amount: transaction.amount,
      checked: transaction.checked,
      accountId: transaction.accountId,
      recurringTransactionId: transaction.recurringTransactionId,
      updatedAt: Value(DateTime.now()),
    );

    final int id = await _db.into(_db.transactions).insert(transactionToAdd);
    return find(id);
  }

  /// Aktualisiert eine Transaktion in der Datenbank
  Future<bool> update(final TransactionsCompanion transaction) async {
    final Transaction? transactionInDb = await find(transaction.id.value);
    final Transaction? transactionData = transactionInDb?.copyWithCompanion(
      transaction,
    );

    final DateTime date = transaction.date.value!.add(
      transaction.date.value!.timeZoneOffset,
    );

    final TransactionsCompanion transactionToUpdate = TransactionsCompanion(
      id: Value(transactionData?.id ?? transaction.id.value),
      name: Value(transactionData?.name ?? transaction.name.value),
      date: Value(date),
      amount: Value(transactionData?.amount ?? transaction.amount.value),
      checked: const Value(true),
      accountId: Value(
        transactionData?.accountId ?? transaction.accountId.value,
      ),
      recurringTransactionId: Value(
        transactionData?.recurringTransactionId ??
            transaction.recurringTransactionId.value,
      ),
      updatedAt: Value(DateTime.now()),
    );

    return _db.update(_db.transactions).replace(transactionToUpdate);
  }

  /// Entfernt eine Transaktion aus der Datenbank
  Future<int> remove(final Transaction transaction) => (_db.delete(
    _db.transactions,
  )..where((final t) => t.id.equals(transaction.id))).go();

  /// Sucht eine Transaktion anhand seiner Id aus der Datenbank
  Future<Transaction?> find(final int id) => (_db.select(
    _db.transactions,
  )..where((final t) => t.id.equals(id))).getSingleOrNull();

  /// Sucht Transaktionen anhand der gegebenen Parameter aus der Datenbank
  Future<List<Transaction>> findBy({
    final int? id,
    final String? name,
    final DateTime? date,
    final DateTime? dateFrom,
    final DateTime? dateTo,
    final double? amount,
    final double? amountMin,
    final double? amountMax,
    final Account? account,
    final bool? checked,
    final RecurringTransaction? recurringTransaction,
    final int? limit,
    final int? offset,
    final String? orderBy,
  }) {
    final SimpleSelectStatement<$TransactionsTable, Transaction> query = _db
        .select(_db.transactions);

    if (id != null) {
      query.where((final t) => t.id.equals(id));
    }

    if (name != null && name.isNotEmpty) {
      query.where((final t) => t.name.like('%$name%'));
    }

    if (date != null) {
      query.where((final t) => t.date.equals(date));
    }

    if (dateFrom != null) {
      query.where((final t) => t.date.isBiggerThanValue(dateFrom));
    }

    if (dateTo != null) {
      query.where((final t) => t.date.isSmallerThanValue(dateTo));
    }

    if (amount != null) {
      query.where((final t) => t.amount.equals(amount));
    }

    if (amountMin != null) {
      query.where((final t) => t.amount.isBiggerThanValue(amountMin));
    }

    if (amountMax != null) {
      query.where((final t) => t.amount.isSmallerThanValue(amountMax));
    }

    if (account != null) {
      query.where((final t) => t.accountId.equals(account.id));
    }

    if (checked != null) {
      query.where((final t) => t.checked.equals(checked));
    }

    if (recurringTransaction != null) {
      query.where(
        (final t) => t.recurringTransactionId.equals(recurringTransaction.id),
      );
    }

    if (limit != null) {
      query.limit(limit, offset: offset);
    }

    if (orderBy != null) {
      switch (orderBy) {
        case 'nameAsc':
          query.orderBy([(final t) => OrderingTerm.asc(t.name)]);
        case 'nameDesc':
          query.orderBy([(final t) => OrderingTerm.desc(t.name)]);
        case 'dateAsc':
          query.orderBy([(final t) => OrderingTerm.asc(t.date)]);
        case 'dateDesc':
          query.orderBy([(final t) => OrderingTerm.desc(t.date)]);
        case 'amountAsc':
          query.orderBy([(final t) => OrderingTerm.asc(t.amount)]);
        case 'amountDesc':
          query.orderBy([(final t) => OrderingTerm.desc(t.amount)]);
      }
    }

    return query.get();
  }

  /// Gibt den Kontostand zurück
  Future<double> balance({
    final Account? account,
    final DateTime? until,
  }) async {
    final JoinedSelectStatement<$TransactionsTable, Transaction> query =
        _db.selectOnly(_db.transactions)
          ..addColumns([_db.transactions.amount.sum()]);

    if (account != null) {
      query.where(_db.transactions.accountId.equals(account.id));
    }

    if (until != null) {
      query.where(_db.transactions.date.isSmallerOrEqualValue(until));
    }

    return (await query
            .map((final row) => row.read(_db.transactions.amount.sum()) ?? 0)
            .getSingle()) *
        -1;
  }

  /// Gibt den Kontostand der letzten 12 Monate zurück
  Future<List<Map<String, dynamic>>> monthlyBalances({
    final Account? account,
    final String? name,
    final double? amountMin,
    final double? amountMax,
    DateTime? dateFrom,
    DateTime? dateTo,
  }) async {
    final now = DateTime.now();

    final monthStart = DateTime(now.year, now.month - 12);
    final monthEnd = DateTime(now.year, now.month + 1, 0);

    if (dateFrom == null || dateFrom.compareTo(monthStart) < 0) {
      dateFrom = monthStart;
    }

    if (dateTo == null || dateTo.compareTo(monthEnd) > 0) {
      dateTo = monthEnd;
    }

    final Expression<int> yearExpr = _db.transactions.date.year;
    final Expression<int> monthExpr = _db.transactions.date.month;
    final Expression<double> sumExpr = _db.transactions.amount.sum();

    final JoinedSelectStatement<$TransactionsTable, Transaction> query =
        _db.selectOnly(_db.transactions)
          ..addColumns([yearExpr, monthExpr, sumExpr])
          ..groupBy([yearExpr, monthExpr])
          ..orderBy([OrderingTerm.asc(yearExpr), OrderingTerm.asc(monthExpr)])
          ..where(_db.transactions.date.isBiggerOrEqualValue(dateFrom))
          ..where(_db.transactions.date.isSmallerOrEqualValue(dateTo));

    if (account != null) {
      query.where(_db.transactions.accountId.equals(account.id));
    }

    if (name != null && name.isNotEmpty) {
      query.where(_db.transactions.name.like('%$name%'));
    }

    if (amountMin != null) {
      query.where(_db.transactions.amount.isBiggerOrEqualValue(amountMin));
    }

    if (amountMax != null) {
      query.where(_db.transactions.amount.isSmallerOrEqualValue(amountMax));
    }

    final List<Map<String, Object>> rows = (await query.get()).map((final row) {
      final int year = row.read(yearExpr)!;
      final int month = row.read(monthExpr)!;

      return {
        'date': DateTime(year, month),
        'balance': (row.read(sumExpr) ?? 0) * -1,
      };
    }).toList();

    double amount = await balance(account: account, until: dateFrom);

    DateTime dateTimeLoop = dateFrom;
    int loop = 0;

    final List<Map<String, Object>> result = [];

    while (dateTimeLoop.compareTo(monthEnd) < 0) {
      Map<String, Object>? row;
      for (final value in rows) {
        final Object? rowDate = value['date'];

        if (rowDate is DateTime) {
          if (dateTimeLoop.compareTo(rowDate) == 0) {
            row = value;
          }
        }
      }

      if (row == null) {
        result.add({'date': dateTimeLoop, 'balance': 0.0});
      } else {
        result.add(row);
      }

      final double balance = double.parse(result[loop]['balance'].toString());
      amount = balance + amount;

      result[loop]['balance'] = amount;

      loop = loop + 1;
      dateTimeLoop = DateTime(dateTimeLoop.year, dateTimeLoop.month + 1);
    }

    return result;
  }

  /// Markiert die übergebenen Transaktionen als überprüft
  void markTransactionsAsChecked(final List<Transaction> transactions) {
    for (final value in transactions) {
      final TransactionsCompanion transaction = TransactionsCompanion(
        id: Value(value.id),
        date: Value(value.date),
        checked: const Value(true),
      );
      unawaited(update(transaction));
    }
  }
}
