import 'item.dart';
import 'status_effect.dart';
import 'stat_modifier.dart';
import '../enums.dart';
import '../data/item_table.dart';
import '../config/game_config.dart';

class Character {
  String name;
  int hp;
  int baseMaxHp;
  int armor; // Current temporary shield/armor points in battle
  int baseAtk;
  int baseDefense; // Base defense stat
  int baseDodge; // New: Base dodge chance (e.g. 1 = 1%)

  int gold; // New: Currency
  String? image; // New: Image path
  Map<EquipmentSlot, Item> equipment = {};
  List<Item> inventory = [];
  final int maxInventorySize = GameConfig.maxInventorySize;

  // Active status effects
  List<StatusEffect> statusEffects = [];

  // Permanent stat modifiers (e.g. Ascension, Potions)
  List<PermanentStatModifier> permanentModifiers = [];

  Character({
    required this.name,
    int? hp,
    required int maxHp,
    required this.armor,
    required int atk,
    this.baseDefense = 0,
    this.baseDodge = 1,
    this.gold = 0,
    this.image,
  }) : baseMaxHp = maxHp,
       baseAtk = atk,
       hp = hp ?? maxHp;

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'hp': hp,
      'baseMaxHp': baseMaxHp,
      'armor': armor,
      'baseAtk': baseAtk,
      'baseDefense': baseDefense,
      'baseDodge': baseDodge,
      'gold': gold,
      'image': image,
      'equipment': equipment.map(
        (key, value) => MapEntry(equipmentSlotStorageName(key), value.id),
      ),
      'inventory': inventory.map((e) => e.id).toList(),
      'statusEffects': statusEffects.map((e) => e.toJson()).toList(),
      'permanentModifiers': permanentModifiers.map((e) => e.toJson()).toList(),
    };
  }

  factory Character.fromJson(Map<String, dynamic> json) {
    final char = Character(
      name: json['name'],
      hp: json['hp'],
      maxHp: json['baseMaxHp'],
      armor: json['armor'],
      atk: json['baseAtk'],
      baseDefense: json['baseDefense'],
      baseDodge: json['baseDodge'] ?? 1,
      gold: json['gold'],
      image: json['image'],
    );

    // Restore Equipment
    final equipMap = json['equipment'] as Map<String, dynamic>;
    equipMap.forEach((slotName, itemId) {
      final template = ItemTable.get(itemId);
      if (template != null) {
        final slot = equipmentSlotFromName(slotName);
        char.equipment[slot] = template.createItem();
      }
    });

    // Restore Inventory
    final invList = json['inventory'] as List<dynamic>;
    for (var itemId in invList) {
      final template = ItemTable.get(itemId);
      if (template != null) {
        char.inventory.add(template.createItem());
      }
    }

    // Restore Status Effects
    if (json['statusEffects'] != null) {
      char.statusEffects = (json['statusEffects'] as List)
          .map((e) => StatusEffect.fromJson(e))
          .toList();
    }

    // Restore Permanent Modifiers
    if (json['permanentModifiers'] != null) {
      char.permanentModifiers = (json['permanentModifiers'] as List)
          .map((e) => PermanentStatModifier.fromJson(e))
          .toList();
    }

    return char;
  }

  /// Adds a status effect. If it already exists, it refreshes duration or stacks based on logic.
  void addStatusEffect(StatusEffect newEffect) {
    var existing = statusEffects
        .where((e) => e.type == newEffect.type)
        .firstOrNull;

    if (existing != null) {
      if (existing.type == StatusEffectType.bleed) {
        // Stack bleed damage
        existing.value += newEffect.value;
        existing.stacks += 1;
        
        // Cap at 30 (10 stacks of 3)
        if (existing.value > 30) {
          existing.value = 30;
        }
        if (existing.stacks > 10) {
          existing.stacks = 10;
        }
        
        // Always refresh duration for bleed stacking
        existing.duration = newEffect.duration;
      } else {
        // For other effects, just refresh duration if longer
        if (newEffect.duration > existing.duration) {
          existing.duration = newEffect.duration;
        }
      }
    } else {
      // Create a copy of the new effect to avoid reference issues if it comes from an item template
      statusEffects.add(StatusEffect(
        type: newEffect.type,
        duration: newEffect.duration,
        value: newEffect.value,
        stacks: newEffect.stacks,
      ));
    }
  }

  /// Decrements duration of start-of-turn effects (Bleed, Stun).
  void updateStartOfTurnStatusEffects() {
    for (var effect in statusEffects) {
      if (effect.type == StatusEffectType.bleed || effect.type == StatusEffectType.stun) {
        effect.duration--;
      }
    }
    statusEffects.removeWhere((e) => e.duration <= 0);
  }

  /// Decrements duration of end-of-turn effects (all others).
  void updateEndOfTurnStatusEffects() {
    for (var effect in statusEffects) {
      if (effect.type != StatusEffectType.bleed && effect.type != StatusEffectType.stun) {
        effect.duration--;
      }
    }
    statusEffects.removeWhere((e) => e.duration <= 0);
  }

  void addPermanentModifier(PermanentStatModifier modifier) {
    permanentModifiers.add(modifier);
  }

  void removePermanentModifier(String id) {
    permanentModifiers.removeWhere((m) => m.id == id);
  }

  /// Helper to check if character has a specific status
  bool hasStatus(StatusEffectType type) {
    return statusEffects.any((e) => e.type == type);
  }

  int get totalMaxHp {
    int bonus = equipment.values.fold(0, (sum, item) => sum + item.hpBonus);
    return baseMaxHp + bonus;
  }

  int get totalAtk {
    int bonus = equipment.values.fold(0, (sum, item) => sum + item.atkBonus);
    int finalAtk = baseAtk + bonus;

    // Apply Attack Up Buff
    var attackBuff = statusEffects
        .where((e) => e.type == StatusEffectType.attackUp)
        .firstOrNull;
    if (attackBuff != null) {
      // Assuming value is Flat bonus based on ItemTemplate
      finalAtk += attackBuff.value;
    }

    if (hasStatus(StatusEffectType.disarmed)) {
      finalAtk = (finalAtk * GameConfig.disarmedDamageMultiplier).toInt();
    }
    return finalAtk;
  }

  int get totalDefense {
    int bonus = equipment.values.fold(0, (sum, item) => sum + item.armorBonus);
    return baseDefense + bonus;
  }

  int get totalDodge {
    int bonus = equipment.values.fold(0, (sum, item) => sum + item.dodge);
    return baseDodge + bonus;
  }

  int get totalLuck {
    return equipment.values.fold(0, (sum, item) => sum + item.luck);
  }

  bool get isDead => hp <= 0;

  // Adds an item to inventory, returns true if successful, false if inventory is full
  bool addToInventory(Item item) {
    if (inventory.length < maxInventorySize) {
      inventory.add(item);
      return true;
    }
    return false;
  }

  // Equips an item (swapping if necessary)
  // Returns true if successful
  bool equip(Item newItem) {
    return equipToSlot(newItem, newItem.defaultEquipSlot);
  }

  bool equipToSlot(Item newItem, EquipmentSlot targetSlot) {
    if (!inventory.contains(newItem)) return false;
    if (!newItem.canEquipTo(targetSlot)) return false;

    // Check inventory capacity before unequipping multiple items
    int itemsToUnequip = 0;
    if (equipment.containsKey(targetSlot)) itemsToUnequip++;
    
    if (targetSlot == EquipmentSlot.weapon && newItem.weaponType == WeaponType.twoHanded) {
      if (equipment.containsKey(EquipmentSlot.shield)) itemsToUnequip++;
    } else if (targetSlot == EquipmentSlot.shield) {
      if (equipment.containsKey(EquipmentSlot.weapon) && equipment[EquipmentSlot.weapon]!.weaponType == WeaponType.twoHanded) {
        itemsToUnequip++;
      }
    }
    
    // newItem leaves inventory (-1), itemsToUnequip go to inventory (+itemsToUnequip)
    if (inventory.length - 1 + itemsToUnequip > maxInventorySize) {
      return false; // Not enough space
    }

    // 1. Calculate current HP ratio before any changes
    double hpRatio = totalMaxHp > 0
        ? hp / totalMaxHp
        : 0.0; // Avoid division by zero

    // 2. Handle 2H / 1H rules
    if (targetSlot == EquipmentSlot.weapon && newItem.weaponType == WeaponType.twoHanded) {
      // If equipping 2H weapon, unequip shield slot if occupied
      if (equipment.containsKey(EquipmentSlot.shield)) {
        Item shieldItem = equipment[EquipmentSlot.shield]!;
        equipment.remove(EquipmentSlot.shield);
        inventory.add(shieldItem);
      }
    } else if (targetSlot == EquipmentSlot.shield) {
      // If equipping to shield slot, check if main weapon is 2H
      if (equipment.containsKey(EquipmentSlot.weapon)) {
        Item mainWeapon = equipment[EquipmentSlot.weapon]!;
        if (mainWeapon.weaponType == WeaponType.twoHanded) {
          // Unequip 2H weapon
          equipment.remove(EquipmentSlot.weapon);
          inventory.add(mainWeapon);
        }
      }
    }

    // 3. Handle Swap: If slot is occupied, unequip the old item first
    if (equipment.containsKey(targetSlot)) {
      Item oldItem = equipment[targetSlot]!;
      equipment.remove(targetSlot);
      inventory.add(oldItem);
    }

    // 4. Move new item: Inventory -> Equipment
    inventory.remove(newItem);
    equipment[targetSlot] = newItem;

    // 4. Update current HP based on the new totalMaxHp and previous ratio
    hp = (totalMaxHp * hpRatio).toInt();
    if (hp < 0) hp = 0; // Ensure HP does not go below zero
    if (hp > totalMaxHp) {
      hp =
          totalMaxHp; // Final Safety Clamp, though hpRatio <= 1.0 should prevent this
    }

    return true;
  }

  // Unequips an item
  // Returns true if successful (inventory has space)
  bool unequip(Item item) {
    if (!equipment.containsValue(item)) return false;

    final slot = equipment.entries
        .firstWhere((entry) => entry.value == item)
        .key;

    // 1. Calculate current HP ratio before any changes
    double hpRatio = totalMaxHp > 0
        ? hp / totalMaxHp
        : 0.0; // Avoid division by zero

    if (inventory.length < maxInventorySize) {
      equipment.remove(slot);
      inventory.add(item);

      // 2. Update current HP based on the new totalMaxHp and previous ratio
      hp = (totalMaxHp * hpRatio).toInt();
      if (hp < 0) hp = 0; // Ensure HP does not go below zero
      if (hp > totalMaxHp) {
        hp =
            totalMaxHp; // Final Safety Clamp, though hpRatio <= 1.0 should prevent this
      }
      return true;
    }

    return false;
  }

  void heal(int amount) {
    if (isDead) return; // Cannot heal if dead

    hp += amount;
    if (hp > totalMaxHp) {
      hp = totalMaxHp;
    }
  }
}
