using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Domain.Entities.Common.ValueObject;
using Domain.Entities.Members;
using Domain.Entities.Wallets.Policy;
using Domain.Entities.Wallets.ValueObject;

namespace Domain.Entities.Wallets
{
    public class Wallet
    {
        [ForeignKey(nameof(MemberID))]
        public virtual Member Member { get; private set; } = null!;

        private readonly List<WalletBalance> _balances = [];
        public IReadOnlyCollection<WalletBalance> Balances => _balances;

        private readonly List<WalletTransaction> _transactions = [];
        public IReadOnlyCollection<WalletTransaction> Transactions => _transactions;

        [Key]
        public int ID { get; private set; }

        public Guid WalletKey { get; private set; } = Guid.NewGuid();

        public int MemberID { get; private set; }

        public DateTime? UpdatedAt { get; private set; }

        public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;

        private Wallet() { }

        private Wallet(int memberID)
        {
            if (memberID <= 0)
            {
                throw new ArgumentOutOfRangeException(nameof(memberID));
            }

            MemberID = memberID;

            // 지갑 생성 시 구분별 잔액 기본 생성
            EnsureBalance(WalletBalanceType.PgCharged);
            EnsureBalance(WalletBalanceType.Deposit);
            EnsureBalance(WalletBalanceType.Donation);
            EnsureBalance(WalletBalanceType.Reward);
            EnsureBalance(WalletBalanceType.Airdrop);
            EnsureBalance(WalletBalanceType.Locked);
            EnsureBalance(WalletBalanceType.Adjusted);
        }

        public static Wallet Create(int memberID) => new(memberID);

        public Money GetBalance(WalletBalanceType type) => GetBalanceEntity(type).Amount;

        public Money GetTotalAvailable()
        {
            var total = Money.KRW(0);
            foreach (var b in Balances.Where(x => x.Type != WalletBalanceType.Locked))
                total += b.Amount;

            return total;
        }

        // ---- Credit ----
        public void CreditPgCharge(Money amount, string reason = "PG_CHARGE", string? refID = null)
            => Credit(WalletBalanceType.PgCharged, WalletTransactionType.Charge, amount, reason, refID);

        public void CreditDonationIn(Money amount, string reason, string? refID = null)
            => Credit(WalletBalanceType.Donation, WalletTransactionType.DonationIn, amount, reason, refID);

        public void CreditReward(Money amount, string reason = "REWARD", string? refID = null)
            => Credit(WalletBalanceType.Reward, WalletTransactionType.RewardEarned, amount, reason, refID);

        /// <summary>
        /// 가입 축하 보상 적립: 지정 잔액 유형(코인=Airdrop, 캐시=Adjusted)으로 입금하되
        /// 트랜잭션 타입은 RewardEarned 로 기록해 멱등 판정(RefID "signup:{memberID}")을 일원화한다.
        /// Locked 는 출금 프로세스 전용이라 직접 적립 불가.
        /// </summary>
        public void CreditSignupReward(WalletBalanceType type, Money amount, string reason, string? refID = null)
        {
            if (type == WalletBalanceType.Locked)
            {
                throw new ArgumentException("Locked balance cannot be credited directly.", nameof(type));
            }

            Credit(type, WalletTransactionType.RewardEarned, amount, reason, refID);
        }

        private void Credit(
            WalletBalanceType balanceType,
            WalletTransactionType txType,
            Money amount,
            string reason,
            string? refID
        ) {
            EnsureMoney(amount);

            var balance = EnsureBalance(balanceType);
            balance.Increase(amount);

            _transactions.Add(WalletTransaction.Create(
                walletKey: WalletKey,
                balanceType: balanceType,
                txType: txType,
                amount: amount,
                balanceAfter: balance.Amount,
                reason: reason,
                refID: refID
            ));
        }

        // ---- Debit ----
        public void DebitDonationOut(Money amount, string reason, string? refID = null)
            => DebitSingle(WalletBalanceType.Donation, WalletTransactionType.DonationOut, amount, reason, refID);

        // ---- Spend (정책 순서대로 분할 차감) ----
        public void Spend(Money amount, string reason = "SPEND", string? refID = null)
        {
            EnsureMoney(amount);

            var remaining = amount;

            foreach (var type in SpendPolicy.DefaultSpendOrder)
            {
                if (remaining.IsZero) break;

                var balance = EnsureBalance(type);
                if (balance.Amount.IsZero) continue;

                var takeValue = Math.Min(balance.Amount.Value, remaining.Value);
                if (takeValue <= 0) continue;

                var take = Money.KRW(takeValue);

                balance.Decrease(take);
                remaining = remaining - take;

                _transactions.Add(WalletTransaction.Create(
                    walletKey: WalletKey,
                    balanceType: type,
                    txType: WalletTransactionType.Spend,
                    amount: take,
                    balanceAfter: balance.Amount,
                    reason: reason,
                    refID: refID
                ));
            }

            if (!remaining.IsZero)
            {
                throw new InvalidOperationException("Insufficient balance for spending.");
            }
        }

        // ---- Lock/Unlock ----
        public void LockForWithdrawal(Money amount, string reason = "WITHDRAW_REQUEST", string? refID = null)
        {
            EnsureMoney(amount);

            var from = EnsureBalance(WalletBalanceType.Donation);
            var locked = EnsureBalance(WalletBalanceType.Locked);

            from.Decrease(amount);
            _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Donation, WalletTransactionType.Lock, amount, from.Amount, reason, refID));

            locked.Increase(amount);
            _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Locked, WalletTransactionType.Lock, amount, locked.Amount, reason, refID));
        }

        public void UnlockWithdrawal(Money amount, string reason = "WITHDRAW_CANCEL", string? refID = null)
        {
            EnsureMoney(amount);

            var locked = EnsureBalance(WalletBalanceType.Locked);
            var donation = EnsureBalance(WalletBalanceType.Donation);

            locked.Decrease(amount);
            _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Locked, WalletTransactionType.Unlock, amount, locked.Amount, reason, refID));

            donation.Increase(amount);
            _transactions.Add(WalletTransaction.Create(WalletKey, WalletBalanceType.Donation, WalletTransactionType.Unlock, amount, donation.Amount, reason, refID));
        }

        // ---- Adjust ----
        public void AdjustIncrease(Money amount, string reason, string? refID = null, string? memo = null)
        {
            EnsureMoney(amount);

            if (string.IsNullOrWhiteSpace(reason))
            {
                throw new ArgumentException("Adjustment reason is required.", nameof(reason));
            }

            var balance = EnsureBalance(WalletBalanceType.Adjusted);
            balance.Increase(amount);

            _transactions.Add(WalletTransaction.Create(
                walletKey: WalletKey,
                balanceType: WalletBalanceType.Adjusted,
                txType: WalletTransactionType.Adjusted,
                amount: amount,
                balanceAfter: balance.Amount,
                reason: $"ADJUST_IN:{reason}",
                refID: refID,
                memo: memo
            ));
        }

        public void AdjustDecrease(Money amount, string reason, string? refID = null, string? memo = null)
        {
            EnsureMoney(amount);

            if (string.IsNullOrWhiteSpace(reason))
            {
                throw new ArgumentException("Adjustment reason is required.", nameof(reason));
            }

            var balance = EnsureBalance(WalletBalanceType.Adjusted);
            balance.Decrease(amount);

            _transactions.Add(WalletTransaction.Create(
                walletKey: WalletKey,
                balanceType: WalletBalanceType.Adjusted,
                txType: WalletTransactionType.Adjusted,
                amount: amount,
                balanceAfter: balance.Amount,
                reason: $"ADJUST_OUT:{reason}",
                refID: refID,
                memo: memo
            ));
        }

        // ---- Internal ----
        private void DebitSingle(WalletBalanceType balanceType, WalletTransactionType txType, Money amount, string reason, string? refID)
        {
            EnsureMoney(amount);

            var balance = EnsureBalance(balanceType);
            balance.Decrease(amount);

            _transactions.Add(WalletTransaction.Create(
                walletKey: WalletKey,
                balanceType: balanceType,
                txType: txType,
                amount: amount,
                balanceAfter: balance.Amount,
                reason: reason,
                refID: refID
            ));
        }

        private WalletBalance GetBalanceEntity(WalletBalanceType type)
        {
            var found = _balances.SingleOrDefault(x => x.Type == type);
            if (found is null)
            {
                throw new InvalidOperationException($"Balance type '{type}' not initialized.");
            }

            return found;
        }

        private WalletBalance EnsureBalance(WalletBalanceType type)
        {
            var found = _balances.SingleOrDefault(x => x.Type == type);
            if (found != null)
            {
                return found;
            }

            var created = WalletBalance.Create(WalletKey, type);
            _balances.Add(created);

            return created;
        }

        private static void EnsureMoney(Money amount)
        {
            if (amount.IsZero || amount.Value <= 0)
            {
                throw new ArgumentException("Amount must be positive.", nameof(amount));
            }
        }
    }
}