import 'dart:convert';
import 'dart:math';

import 'package:guru_app/database/guru_db.dart';
import 'package:guru_app/financial/product/product_model.dart';
import 'package:guru_utils/database/batch/batch_data.dart';
import 'package:guru_utils/datetime/datetime_utils.dart';
import 'package:guru_utils/id/identifiable.dart';
import 'package:guru_utils/log/log.dart';
import 'package:guru_utils/manifest/manifest.dart';
import 'package:sqflite/sqflite.dart';
import 'package:json_annotation/json_annotation.dart';

part 'inventory_database.g.dart';

@JsonSerializable()
class LimitedBalance {
  @JsonKey(name: "a", defaultValue: 0)
  final int amount;

  @JsonKey(name: "e", defaultValue: -1)
  final int expireAt;

  @JsonKey(name: "r", defaultValue: false)
  final bool recycle;

  LimitedBalance(this.amount, this.expireAt, {this.recycle = false});

  factory LimitedBalance.fromJson(Map<String, dynamic> json) => _$LimitedBalanceFromJson(json);

  Map<String, dynamic> toJson() => _$LimitedBalanceToJson(this);
}

@JsonSerializable()
class TimeSensitiveData {
  @JsonKey(name: "v", defaultValue: [])
  final List<LimitedBalance> valid;

  @JsonKey(name: "e", defaultValue: [])
  final List<LimitedBalance> expired;

  int get validBalance =>
      valid.isEmpty ? 0 : valid.map((e) => e.amount).reduce((total, amount) => total + amount);

  int get expiredBalance =>
      expired.isEmpty ? 0 : expired.map((e) => e.amount).reduce((total, amount) => total + amount);

  const TimeSensitiveData(
      {this.valid = const <LimitedBalance>[], this.expired = const <LimitedBalance>[]});

  TimeSensitiveData.create({LimitedBalance? balance})
      : valid = ((balance?.expireAt ?? 0) > DateTimeUtils.currentTimeInMillis()) ? [balance!] : [],
        expired = [];

  factory TimeSensitiveData.fromJson(Map<String, dynamic> json) =>
      _$TimeSensitiveDataFromJson(json);

  Map<String, dynamic> toJson() => _$TimeSensitiveDataToJson(this);

  TimeSensitiveData attachLimitedBalance(LimitedBalance balance) {
    if (balance.expireAt > DateTimeUtils.currentTimeInMillis()) {
      return TimeSensitiveData(valid: List.of(valid)..add(balance), expired: List.of(expired));
    }
    return this;
  }

  ///
  TimeSensitiveData consume(int balance) {
    final now = DateTimeUtils.currentTimeInMillis();
    final newValid = <LimitedBalance>[];
    for (var item in valid) {
      int amount = item.amount;
      if (now < item.expireAt) {
        if (balance >= item.amount) {
          balance -= item.amount;
          continue;
        } else {
          amount -= balance;
          balance = 0;
        }
      }
      // 这里只针对未过期的道具进行处理
      newValid.add(LimitedBalance(amount, item.expireAt));
    }
    newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt));
    return TimeSensitiveData(valid: newValid, expired: expired);
  }

  RecycleResult recycleExpiredBalance({int? transactionTs}) {
    final now = transactionTs ?? DateTimeUtils.currentTimeInMillis();
    final newValid = <LimitedBalance>[];
    final newExpired = <LimitedBalance>[];
    int expiredBalance = 0;
    for (var item in valid) {
      if (now >= item.expireAt) {
        expiredBalance += item.amount;
        if (item.recycle) {
          newExpired.add(item);
        }
      } else {
        newValid.add(item);
      }
    }
    newValid.sort((a, b) => b.expireAt.compareTo(a.expireAt));
    return RecycleResult(expiredBalance, TimeSensitiveData(valid: newValid, expired: newExpired));
  }
}

class RecycleResult {
  final int expiredBalance;
  final TimeSensitiveData timeSensitiveData;

  RecycleResult(this.expiredBalance, this.timeSensitiveData);
}

class ConsumeResult {
  final bool consumed;
  final InventoryItem item;

  ConsumeResult.success(this.item) : consumed = true;

  ConsumeResult.error(this.item) : consumed = false;
}

@JsonSerializable(constructor: "_")
class InventoryDetails {
  @JsonKey(name: "a", defaultValue: {})
  final Map<TransactionMethod, int> acquired;

  @JsonKey(name: "c", defaultValue: {})
  final Map<String, int> consumed;

  @JsonKey(name: "d")
  final Map<String, dynamic> data;

  const InventoryDetails._(this.acquired, this.consumed, this.data);

  InventoryDetails.empty()
      : acquired = const <TransactionMethod, int>{},
        consumed = const <String, int>{},
        data = const <String, dynamic>{};

  InventoryDetails.create(int amount)
      : acquired = (amount == 0)
            ? const <TransactionMethod, int>{}
            : <TransactionMethod, int>{TransactionMethod.unknown: amount},
        consumed = const <String, int>{},
        data = <String, dynamic>{};

  // 通过什么方式获得了多少
  InventoryDetails acquire(TransactionMethod method, int amount) {
    return InventoryDetails._(
        Map<TransactionMethod, int>.from(acquired)..[method] = (acquired[method] ?? 0) + amount,
        Map<String, int>.from(consumed),
        Map<String, dynamic>.from(data));
  }

  // 在什么场景消耗了多少
  InventoryDetails consume(String scene, int amount) {
    return InventoryDetails._(
        Map<TransactionMethod, int>.from(acquired),
        Map<String, int>.from(consumed)..[scene] = (consumed[scene] ?? 0) + amount,
        Map<String, dynamic>.from(data));
  }

  factory InventoryDetails.fromJson(Map<String, dynamic> json) => _$InventoryDetailsFromJson(json);

  Map<String, dynamic> toJson() => _$InventoryDetailsToJson(this);

  void setInt(String key, int value) {
    data[key] = value;
  }

  void setDouble(String key, double value) {
    data[key] = value;
  }

  void setString(String key, String value) {
    data[key] = value;
  }

  void setBool(String key, bool value) {
    data[key] = value;
  }

  int? getInt(String key) {
    return data[key];
  }

  double? getDouble(String key) {
    return data[key];
  }

  String? getString(String key) {
    return data[key];
  }

  bool? getBool(String key) {
    return data[key];
  }
}

class InventoryDetailsStringConvert implements JsonConverter<InventoryDetails, String> {
  const InventoryDetailsStringConvert();

  @override
  InventoryDetails fromJson(String json) {
    if (json.isEmpty) {
      return InventoryDetails.empty();
    }
    final result = jsonDecode(json);
    return InventoryDetails.fromJson(result);
  }

  @override
  String toJson(InventoryDetails transaction) {
    return jsonEncode(transaction);
  }
}

class TimeSensitiveDataStringConvert implements JsonConverter<TimeSensitiveData, String> {
  const TimeSensitiveDataStringConvert();

  @override
  TimeSensitiveData fromJson(String json) {
    if (json.isEmpty) {
      return const TimeSensitiveData();
    }
    final result = jsonDecode(json);
    return TimeSensitiveData.fromJson(result);
  }

  @override
  String toJson(TimeSensitiveData transaction) {
    return jsonEncode(transaction);
  }
}

const InventoryDetailsStringConvert inventoryDetailsStringConvert = InventoryDetailsStringConvert();
const TimeSensitiveDataStringConvert timeSensitiveDataStringConvert =
    TimeSensitiveDataStringConvert();

class InventoryTable {
  static const tbName = "inventory"; // Product Transaction Table
  static const dbSku = "sku";
  static const dbBalance = "balance";
  static const dbCategory = "cat";
  static const dbAttr = "attr";
  static const dbDetails = "details";
  static const dbTimeSensitive = "tsv";
  static const dbUpdateAt = "update_at";
  static const dbCreateAt = "create_at";

  static Future createTable(Transaction delegate) async {
    const v1Fields = "${InventoryTable.dbSku} TEXT PRIMARY KEY,"
        "${InventoryTable.dbBalance} INTEGER NOT NULL DEFAULT 0,"
        "${InventoryTable.dbCategory} TEXT NOT NULL DEFAULT '',"
        "${InventoryTable.dbAttr} INTEGER NOT NULL DEFAULT ${DetailsAttr.consumable},"
        "${InventoryTable.dbDetails} TEXT NOT NULL DEFAULT '',"
        "${InventoryTable.dbTimeSensitive} TEXT NOT NULL DEFAULT '',"
        "${InventoryTable.dbCreateAt} INTEGER NOT NULL DEFAULT 0,"
        "${InventoryTable.dbUpdateAt} INTEGER NOT NULL DEFAULT 0";

    const cmd = "CREATE TABLE ${InventoryTable.tbName} ("
        "$v1Fields"
        ");";

    Log.v("#### cmd: $cmd");

    await delegate.execute(cmd);
    await delegate.execute(
        "CREATE INDEX inventory_item_idx ON ${InventoryTable.tbName} (${InventoryTable.dbSku});");
    await delegate.execute(
        "CREATE INDEX inventory_item_category_idx ON ${InventoryTable.tbName} (${InventoryTable.dbCategory});");
  }
}

@JsonSerializable(constructor: "_")
class InventoryItem implements Identifiable {
  @JsonKey(name: InventoryTable.dbSku)
  final String sku;

  @JsonKey(name: InventoryTable.dbBalance)
  final int balance; // 永久的+时效

  @JsonKey(name: InventoryTable.dbCategory)
  final String category;

  @JsonKey(name: InventoryTable.dbAttr)
  final int attr;

  @JsonKey(name: InventoryTable.dbTimeSensitive)
  @timeSensitiveDataStringConvert
  final TimeSensitiveData timeSensitive;

  @JsonKey(name: InventoryTable.dbUpdateAt)
  final int updateAt;

  @JsonKey(name: InventoryTable.dbCreateAt)
  final int createAt;

  @JsonKey(name: InventoryTable.dbDetails)
  @inventoryDetailsStringConvert
  final InventoryDetails details;

  InventoryItem._(this.sku, this.balance, this.category, this.attr, this.details,
      this.timeSensitive, this.createAt, this.updateAt);

  InventoryItem.create(this.sku, this.category, this.attr,
      {int expireAt = -1, this.balance = 0, TransactionMethod method = TransactionMethod.unknown})
      : updateAt = DateTimeUtils.currentTimeInMillis(),
        createAt = DateTimeUtils.currentTimeInMillis(),
        details = InventoryDetails.create(balance),
        timeSensitive = TimeSensitiveData.create(balance: LimitedBalance(balance, expireAt));

  InventoryItem acquire(TransactionMethod method, int amount, {int? expiredAt}) {
    final now = DateTimeUtils.currentTimeInMillis();
    if (expiredAt != null && expiredAt > now) {
      timeSensitive.valid.add(LimitedBalance(amount, expiredAt));
    }
    final recycled = timeSensitive.recycleExpiredBalance();
    final target = balance + amount;
    return InventoryItem._(sku, (target - recycled.expiredBalance).clamp(0, target), category, attr,
        details.acquire(method, amount), recycled.timeSensitiveData, createAt, now);
  }

  ConsumeResult consume(int amount, {String scene = ""}) {
    final now = DateTimeUtils.currentTimeInMillis();
    final recycled = timeSensitive.recycleExpiredBalance();
    int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance);
    if (remainBalance >= amount && attr == DetailsAttr.consumable) {
      remainBalance -= amount;
      return ConsumeResult.success(InventoryItem._(
          sku,
          remainBalance,
          category,
          attr,
          details.consume(scene, amount),
          recycled.timeSensitiveData.consume(amount),
          createAt,
          now));
    }
    return ConsumeResult.error(InventoryItem._(
        sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now));
  }

  ConsumeResult consumeTimeSensitiveOnly(int amount, {String scene = "", int? transactionTs}) {
    final now = transactionTs ?? DateTimeUtils.currentTimeInMillis();
    final recycled = timeSensitive.recycleExpiredBalance(transactionTs: transactionTs);
    final validBalance = recycled.timeSensitiveData.validBalance;
    int remainBalance = (balance - recycled.expiredBalance).clamp(0, balance);

    /// 这里只是针对有时效性的道具进行消耗，如果时效性道具不足，将返回错误
    if (validBalance >= amount && attr == DetailsAttr.consumable) {
      /// 虽然这里只是针对时效性道具进行消耗，但是这里的balance还是会进行更新
      remainBalance = (remainBalance - amount).clamp(0, remainBalance);
      return ConsumeResult.success(InventoryItem._(
          sku,
          remainBalance,
          category,
          attr,
          details.consume(scene, amount),
          recycled.timeSensitiveData.consume(amount),
          createAt,
          now));
    }
    return ConsumeResult.error(InventoryItem._(
        sku, remainBalance, category, attr, details, recycled.timeSensitiveData, createAt, now));
  }

  factory InventoryItem.fromJson(Map<String, dynamic> json) => _$InventoryItemFromJson(json);

  Map<String, dynamic> toJson() => _$InventoryItemToJson(this);

  @override
  String get id => sku;
}

extension InventoryDatabase on GuruDB {
  Future<BatchData<InventoryItem>> loadInventoryItems() async {
    final db = getDb();
    final result = await db.rawQuery("SELECT * FROM ${InventoryTable.tbName}");
    final batchData = BatchData<InventoryItem>.empty();
    if (result.isNotEmpty) {
      batchData.queryAll([for (var item in result) InventoryItem.fromJson(item)]);
    }
    return batchData;
  }

  Future<BatchData<InventoryItem>> updateInventoryItem(InventoryItem item) async {
    final db = getDb();
    await db.insert(InventoryTable.tbName, item.toJson(),
        conflictAlgorithm: ConflictAlgorithm.replace);
    return BatchData<InventoryItem>.singleSuccess(BatchMethod.update, item);
  }

  Future<BatchData<InventoryItem>> updateInventoryItems(List<InventoryItem> items) async {
    final BatchData<InventoryItem> batchData = BatchData<InventoryItem>.empty();
    return runInTransaction((txn) async {
      for (var item in items) {
        await txn.insert(InventoryTable.tbName, item.toJson(),
            conflictAlgorithm: ConflictAlgorithm.replace);
        batchData.update(item);
      }
      return batchData;
    });
  }
}
