using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Shared.Constants;
using System.ComponentModel.DataAnnotations;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// Manages the company-wide credit memo register. Credit memos reduce a customer's outstanding
/// balance and can be issued standalone (goodwill, billing correction) or linked to an original
/// invoice (price dispute, rework resolution). Applied portions reduce invoice BalanceDue and
/// customer.CreditBalance atomically inside a transaction.
/// GL entries on Apply: DR 4950 Sales Discounts (contra-revenue) / CR AR — mirrors the treatment
/// of invoice discounts so the Trial Balance and Balance Sheet reflect the applied credit as both
/// a revenue deduction and an AR reduction.
/// </summary>
[Authorize(Policy = AppConstants.Policies.CompanyAdminOnly)]
public class CreditMemosController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ITenantContext _tenantContext;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<CreditMemosController> _logger;
    private readonly IAccountBalanceService _accountBalanceService;

    public CreditMemosController(
        IUnitOfWork unitOfWork,
        ITenantContext tenantContext,
        UserManager<ApplicationUser> userManager,
        ILogger<CreditMemosController> logger,
        IAccountBalanceService accountBalanceService)
    {
        _unitOfWork = unitOfWork;
        _tenantContext = tenantContext;
        _userManager = userManager;
        _logger = logger;
        _accountBalanceService = accountBalanceService;
    }

    /// <summary>Lists all credit memos for the current company with optional status and text filters.</summary>
    [HttpGet]
    public async Task<IActionResult> Index(string? status, string? search)
    {
        var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var memos = await _unitOfWork.CreditMemos.FindAsync(
            m => m.CompanyId == companyId, false,
            m => m.Customer);

        if (!string.IsNullOrWhiteSpace(search))
            memos = memos.Where(m =>
                DisplayName(m.Customer).Contains(search, StringComparison.OrdinalIgnoreCase) ||
                m.MemoNumber.Contains(search, StringComparison.OrdinalIgnoreCase) ||
                m.Reason.Contains(search, StringComparison.OrdinalIgnoreCase)).ToList();

        if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse<CreditMemoStatus>(status, out var parsed))
            memos = memos.Where(m => m.Status == parsed).ToList();

        ViewBag.Status = status ?? "";
        ViewBag.Search = search ?? "";
        ViewBag.ActiveCount = memos.Count(m => m.Status is CreditMemoStatus.Active or CreditMemoStatus.PartiallyApplied);
        ViewBag.OutstandingBalance = memos
            .Where(m => m.Status is not CreditMemoStatus.Voided and not CreditMemoStatus.FullyApplied)
            .Sum(m => m.RemainingBalance);

        return View(memos.OrderByDescending(m => m.IssueDate).ToList());
    }

    /// <summary>Shows a single credit memo with its full application history and an Apply modal for open invoices.</summary>
    [HttpGet]
    public async Task<IActionResult> Details(int id)
    {
        var memo = await _unitOfWork.CreditMemos.GetByIdAsync(
            id, false,
            m => m.Customer,
            m => m.OriginalInvoice,
            m => m.IssuedBy);

        if (memo == null) return NotFound();

        var applications = await _unitOfWork.CreditMemoApplications.FindAsync(
            a => a.CreditMemoId == id, false,
            a => a.Invoice,
            a => a.AppliedBy);

        var openInvoices = await _unitOfWork.Invoices.FindAsync(
            i => i.CustomerId == memo.CustomerId
              && i.Status != InvoiceStatus.Paid
              && i.Status != InvoiceStatus.Voided
              && i.Status != InvoiceStatus.WrittenOff);

        ViewBag.Applications = applications.OrderByDescending(a => a.AppliedDate).ToList();
        ViewBag.OpenInvoices  = openInvoices.Where(i => i.BalanceDue > 0).OrderBy(i => i.DueDate).ToList();
        ViewBag.CanApply      = memo.Status is CreditMemoStatus.Active or CreditMemoStatus.PartiallyApplied
                             && memo.RemainingBalance > 0;

        return View(memo);
    }

    /// <summary>Shows the standalone credit-memo creation form. Accepts optional customerId/invoiceId query params to pre-populate.</summary>
    [HttpGet]
    public async Task<IActionResult> Create(int? customerId, int? invoiceId)
    {
        string? linkedInvoiceNumber = null;
        if (invoiceId.HasValue)
        {
            var inv = await _unitOfWork.Invoices.GetByIdAsync(invoiceId.Value);
            if (inv != null)
            {
                linkedInvoiceNumber = inv.InvoiceNumber;
                customerId ??= inv.CustomerId;
            }
        }

        await PopulateCustomersAsync(customerId);
        ViewBag.LinkedInvoiceNumber = linkedInvoiceNumber;

        return View(new CreditMemoCreateVm
        {
            CustomerId        = customerId ?? 0,
            OriginalInvoiceId = invoiceId
        });
    }

    /// <summary>
    /// Creates a standalone credit memo and immediately increments customer.CreditBalance so the
    /// credit is visible on the customer account before it is applied to any specific invoice.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Create(CreditMemoCreateVm vm)
    {
        if (!ModelState.IsValid)
        {
            await PopulateCustomersAsync(vm.CustomerId);
            return View(vm);
        }

        var customer = await _unitOfWork.Customers.GetByIdAsync(vm.CustomerId);
        if (customer == null)
        {
            ModelState.AddModelError("CustomerId", "Customer not found.");
            await PopulateCustomersAsync(null);
            return View(vm);
        }

        var companyId   = _tenantContext.GetCurrentCompanyId() ?? 0;
        var memoNumber  = await GenerateMemoNumberAsync(companyId);
        var currentUser = await _userManager.GetUserAsync(User);

        var memo = new CreditMemo
        {
            MemoNumber        = memoNumber,
            CustomerId        = vm.CustomerId,
            OriginalInvoiceId = vm.OriginalInvoiceId > 0 ? vm.OriginalInvoiceId : null,
            Amount            = vm.Amount,
            AmountApplied     = 0,
            IssueDate         = DateTime.UtcNow,
            ExpiryDate        = vm.ExpiryDate.HasValue
                                    ? DateTime.SpecifyKind(vm.ExpiryDate.Value, DateTimeKind.Utc)
                                    : null,
            Reason            = vm.Reason,
            Notes             = vm.Notes,
            Status            = CreditMemoStatus.Active,
            IssuedById        = currentUser?.Id,
            CompanyId         = companyId,
            CreatedAt         = DateTime.UtcNow
        };

        await _unitOfWork.CreditMemos.AddAsync(memo);

        customer.CreditBalance += vm.Amount;
        await _unitOfWork.Customers.UpdateAsync(customer);

        await _unitOfWork.CompleteAsync();

        // GL: the credit is a liability owed to the customer — DR Sales Discounts (contra-revenue)
        // / CR Customer Credits (2350). Relieved when the memo is applied to an invoice.
        var discountAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == customer.CompanyId && a.AccountNumber == "4950" && a.IsActive);
        var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == customer.CompanyId && a.AccountNumber == "2350" && a.IsActive);
        await _accountBalanceService.DebitAsync(discountAcct?.Id, vm.Amount);
        await _accountBalanceService.CreditAsync(customerCreditsAcct?.Id, vm.Amount);

        TempData["Success"] = $"Credit memo {memoNumber} for {vm.Amount:C} issued to {DisplayName(customer)}.";
        return RedirectToAction(nameof(Details), new { id = memo.Id });
    }

    /// <summary>
    /// Applies a portion of this credit memo to an open invoice. The applied amount is capped at the
    /// minimum of the requested amount, the memo's RemainingBalance, and the invoice's BalanceDue —
    /// preventing over-application even with concurrent requests. Customer.CreditBalance is reduced
    /// by the same applied amount. Automatically marks the invoice Paid when BalanceDue reaches zero.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Apply(int id, int invoiceId, decimal amount)
    {
        try
        {
            var memo = await _unitOfWork.CreditMemos.GetByIdAsync(id);
            if (memo == null) return NotFound();

            var invoice = await _unitOfWork.Invoices.GetByIdAsync(invoiceId, false, i => i.Customer);
            if (invoice == null)
            {
                TempData["Error"] = "Invoice not found.";
                return RedirectToAction(nameof(Details), new { id });
            }

            if (memo.Status is CreditMemoStatus.Voided or CreditMemoStatus.FullyApplied)
            {
                TempData["Error"] = "Credit memo is not available to apply.";
                return RedirectToAction(nameof(Details), new { id });
            }

            var applyAmount = Math.Min(amount, Math.Min(memo.RemainingBalance, invoice.BalanceDue));
            if (applyAmount <= 0)
            {
                TempData["Error"] = "No applicable amount — invoice may already be paid or credit exhausted.";
                return RedirectToAction(nameof(Details), new { id });
            }

            var currentUser = await _userManager.GetUserAsync(User);

            await _unitOfWork.ExecuteInTransactionAsync(async () =>
            {
                await _unitOfWork.CreditMemoApplications.AddAsync(new CreditMemoApplication
                {
                    CreditMemoId  = id,
                    InvoiceId     = invoiceId,
                    AmountApplied = applyAmount,
                    AppliedDate   = DateTime.UtcNow,
                    AppliedById   = currentUser?.Id,
                    CompanyId     = invoice.CompanyId,
                    CreatedAt     = DateTime.UtcNow
                });

                invoice.CreditApplied += applyAmount;
                await _unitOfWork.Invoices.UpdateAsync(invoice);

                memo.AmountApplied += applyAmount;
                memo.Status = memo.AmountApplied >= memo.Amount
                    ? CreditMemoStatus.FullyApplied
                    : CreditMemoStatus.PartiallyApplied;
                await _unitOfWork.CreditMemos.UpdateAsync(memo);

                if (invoice.Customer != null)
                {
                    invoice.Customer.CreditBalance = Math.Max(0, invoice.Customer.CreditBalance - applyAmount);
                    await _unitOfWork.Customers.UpdateAsync(invoice.Customer);
                }

                if (invoice.BalanceDue <= 0 && invoice.Status != InvoiceStatus.Paid)
                {
                    invoice.Status   = InvoiceStatus.Paid;
                    invoice.PaidDate = DateTime.UtcNow;
                    await _unitOfWork.Invoices.UpdateAsync(invoice);
                }

                // GL: applying the credit relieves the liability — DR Customer Credits (2350) / CR AR.
                // The contra-revenue (Sales Discounts) was recognized when the credit was issued.
                // Keeps Account.CurrentBalance in sync for RecalculateAllAsync and direct readers.
                var arAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                    a => a.CompanyId == invoice.CompanyId && a.AccountSubType == AccountSubType.AccountsReceivable && a.IsActive);
                var customerCreditsAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(
                    a => a.CompanyId == invoice.CompanyId && a.AccountNumber == "2350" && a.IsActive);
                await _accountBalanceService.DebitAsync(customerCreditsAcct?.Id, applyAmount);
                await _accountBalanceService.CreditAsync(arAcct?.Id, applyAmount);

                await _unitOfWork.CompleteAsync();
            });

            TempData["Success"] = $"{applyAmount:C} applied to invoice {invoice.InvoiceNumber}.";
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error applying credit memo {MemoId} to invoice {InvoiceId}", id, invoiceId);
            TempData["Error"] = "An error occurred applying the credit.";
        }

        return RedirectToAction(nameof(Details), new { id });
    }

    /// <summary>
    /// Voids a credit memo and reverses only the unapplied remainder from customer.CreditBalance.
    /// The portion already applied to invoices is NOT reversed — those reductions to BalanceDue are
    /// settled and form part of the immutable audit trail.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Void(int id)
    {
        var memo = await _unitOfWork.CreditMemos.GetByIdAsync(id, false, m => m.Customer);
        if (memo == null) return NotFound();

        if (memo.Status == CreditMemoStatus.Voided)
        {
            TempData["Error"] = "Credit memo is already voided.";
            return RedirectToAction(nameof(Details), new { id });
        }

        var remaining  = memo.Amount - memo.AmountApplied;
        memo.Status    = CreditMemoStatus.Voided;
        memo.UpdatedAt = DateTime.UtcNow;
        await _unitOfWork.CreditMemos.UpdateAsync(memo);

        if (remaining > 0 && memo.Customer != null)
        {
            memo.Customer.CreditBalance = Math.Max(0, memo.Customer.CreditBalance - remaining);
            await _unitOfWork.Customers.UpdateAsync(memo.Customer);
        }

        // GL: reverse the unapplied issuance — DR Customer Credits (2350) / CR Sales Discounts.
        if (remaining > 0)
        {
            var ccAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == memo.CompanyId && a.AccountNumber == "2350" && a.IsActive);
            var sdAcct = await _unitOfWork.Accounts.FirstOrDefaultAsync(a => a.CompanyId == memo.CompanyId && a.AccountNumber == "4950" && a.IsActive);
            await _accountBalanceService.DebitAsync(ccAcct?.Id, remaining);
            await _accountBalanceService.CreditAsync(sdAcct?.Id, remaining);
        }

        await _unitOfWork.CompleteAsync();
        TempData["Success"] = "Credit memo voided. Unapplied balance reversed from customer credit.";
        return RedirectToAction(nameof(Details), new { id });
    }

    private async Task PopulateCustomersAsync(int? selectedId)
    {
        var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var customers = await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId);
        ViewBag.Customers = customers
            .OrderBy(c => c.CompanyName ?? $"{c.ContactFirstName} {c.ContactLastName}".Trim())
            .Select(c => new SelectListItem
            {
                Value    = c.Id.ToString(),
                Text     = c.IsTaxExempt ? $"{DisplayName(c)} ★" : DisplayName(c),
                Selected = c.Id == selectedId
            })
            .ToList();
    }

    /// <summary>
    /// Generates the next sequential memo number in CM-YYMM-#### format.
    /// Uses IgnoreQueryFilters so soft-deleted memos count, preventing number reuse.
    /// </summary>
    private async Task<string> GenerateMemoNumberAsync(int companyId)
    {
        var prefix   = $"CM-{DateTime.UtcNow:yy}{DateTime.UtcNow.Month:D2}-";
        var existing = (await _unitOfWork.CreditMemos.FindAsync(
                m => m.CompanyId == companyId && m.MemoNumber.StartsWith(prefix), true))
            .Select(m => m.MemoNumber)
            .ToList();

        var maxNum = 0;
        foreach (var num in existing)
        {
            var suffix = num.Length >= prefix.Length + 4 ? num[prefix.Length..] : "";
            if (int.TryParse(suffix, out int n) && n > maxNum)
                maxNum = n;
        }
        return $"{prefix}{(maxNum + 1):D4}";
    }

    private static string DisplayName(Customer? c) =>
        c == null ? string.Empty :
        !string.IsNullOrWhiteSpace(c.CompanyName) ? c.CompanyName
        : $"{c.ContactFirstName} {c.ContactLastName}".Trim();
}

public class CreditMemoCreateVm
{
    [Required, Range(1, int.MaxValue, ErrorMessage = "Please select a customer.")]
    public int CustomerId { get; set; }

    [Required, Range(0.01, 1_000_000, ErrorMessage = "Amount must be greater than $0.00.")]
    public decimal Amount { get; set; }

    [Required, MaxLength(500, ErrorMessage = "Reason cannot exceed 500 characters.")]
    public string Reason { get; set; } = string.Empty;

    [MaxLength(2000)]
    public string? Notes { get; set; }

    public DateTime? ExpiryDate { get; set; }

    /// <summary>Optional link to the invoice that prompted this credit (price dispute, billing error, etc.).</summary>
    public int? OriginalInvoiceId { get; set; }
}
