using AutoMapper;
using Microsoft.AspNetCore.Authorization;
using PowderCoating.Shared.Constants;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using PowderCoating.Application.Configuration;
using PowderCoating.Application.Services;
using PowderCoating.Application.DTOs.Accounting;
using PowderCoating.Application.DTOs.AI;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Web.Helpers;

namespace PowderCoating.Web.Controllers;

[Authorize(Policy = AppConstants.Policies.CanViewData)]
public class ExpensesController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly ILogger<ExpensesController> _logger;
    private readonly IAzureBlobStorageService _blobStorage;
    private readonly StorageSettings _storageSettings;
    private readonly IAccountBalanceService _accountBalanceService;
    private readonly IAccountingAiService _accountingAi;
    private readonly IAiUsageLogger _usageLogger;

    private static readonly string[] AllowedReceiptTypes = { ".jpg", ".jpeg", ".png", ".gif", ".webp", ".pdf" };
    private const long MaxReceiptBytes = 10 * 1024 * 1024; // 10 MB

    public ExpensesController(
        IUnitOfWork unitOfWork,
        IMapper mapper,
        UserManager<ApplicationUser> userManager,
        ILogger<ExpensesController> logger,
        IAzureBlobStorageService blobStorage,
        IOptions<StorageSettings> storageSettings,
        IAccountBalanceService accountBalanceService,
        IAccountingAiService accountingAi,
        IAiUsageLogger usageLogger)
    {
        _unitOfWork = unitOfWork;
        _mapper = mapper;
        _userManager = userManager;
        _logger = logger;
        _blobStorage = blobStorage;
        _storageSettings = storageSettings.Value;
        _accountBalanceService = accountBalanceService;
        _accountingAi = accountingAi;
        _usageLogger = usageLogger;
    }

    // ── Index ────────────────────────────────────────────────────────────────

    /// <summary>
    /// Redirects to the unified bills/expenses ledger (<see cref="BillsController.Index"/>)
    /// pre-filtered to Expense entries. The two entry types share a single list view to reduce
    /// navigation surface area; this redirect keeps the <c>/Expenses</c> URL working for
    /// existing bookmarks and links.
    /// </summary>
    public IActionResult Index()
    {
        return RedirectToAction("Index", "Bills", new { type = "Expense" });
    }

    /// <summary>
    /// Legacy standalone expense list — kept for reference but disabled via <c>[NonAction]</c>
    /// since the unified Bills/Expenses index in <see cref="BillsController"/> superseded it.
    /// Do not route traffic here; use <see cref="Index"/> instead.
    /// </summary>
    [NonAction]
    public async Task<IActionResult> IndexLegacy(string? search, int? accountId, DateTime? from, DateTime? to, int page = 1, int pageSize = 25)
    {
        var allExpenses = (await _unitOfWork.Expenses.GetAllAsync(
            false, e => e.Vendor, e => e.ExpenseAccount, e => e.PaymentAccount, e => e.Job))
            .AsEnumerable();

        if (!string.IsNullOrEmpty(search))
            allExpenses = allExpenses.Where(e => e.ExpenseNumber.Contains(search) ||
                (e.Memo != null && e.Memo.Contains(search)) ||
                (e.Vendor != null && e.Vendor.CompanyName.Contains(search)));

        if (accountId.HasValue)
            allExpenses = allExpenses.Where(e => e.ExpenseAccountId == accountId.Value);

        if (from.HasValue)
            allExpenses = allExpenses.Where(e => e.Date >= from.Value);

        if (to.HasValue)
            allExpenses = allExpenses.Where(e => e.Date <= to.Value);

        var expenses = allExpenses.OrderByDescending(e => e.CreatedAt).ToList();
        var dtos = _mapper.Map<List<ExpenseListDto>>(expenses);

        ViewBag.Search = search;
        ViewBag.AccountId = accountId;
        ViewBag.From = from?.ToString("yyyy-MM-dd");
        ViewBag.To = to?.ToString("yyyy-MM-dd");
        ViewBag.TotalAmount = dtos.Sum(e => e.Amount);

        var legacyUser = await _userManager.GetUserAsync(User);
        var expenseAccounts = (await _unitOfWork.Accounts.FindAsync(
                a => a.CompanyId == legacyUser!.CompanyId && a.IsActive &&
                    (a.AccountType == AccountType.Expense || a.AccountType == AccountType.CostOfGoods)))
            .OrderBy(a => a.AccountNumber)
            .ToList();

        ViewBag.AccountFilter = expenseAccounts
            .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
            .ToList();

        return View(dtos);
    }

    // ── Create ───────────────────────────────────────────────────────────────

    /// <summary>
    /// Returns the blank expense creation form. Unlike vendor bills, direct expenses record
    /// an immediate payment (no AP liability step) — the expense account is debited and the
    /// payment account (bank/credit card) is credited at the point of saving.
    /// </summary>
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    public async Task<IActionResult> Create()
    {
        await PopulateDropdownsAsync();
        return View(new CreateExpenseDto { Date = DateTime.Today });
    }

    /// <summary>
    /// Persists a new direct expense. The receipt file (if provided) is uploaded after the
    /// entity is saved so that the <c>Expense.Id</c> is available for the blob path. Double-entry
    /// effect: the expense account is debited (<c>DebitAsync</c>) and the payment account is
    /// credited (<c>CreditAsync</c>). An optional <paramref name="receiptFile"/> is stored in
    /// Azure Blob Storage; upload failure is non-fatal (a warning is shown) so the expense record
    /// is never lost due to a transient storage outage.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    public async Task<IActionResult> Create(CreateExpenseDto dto, IFormFile? receiptFile)
    {
        if (!ModelState.IsValid)
        {
            await PopulateDropdownsAsync();
            return View(dto);
        }

        if (receiptFile != null)
        {
            var (receiptValid, _, receiptError) = BlobFileHelper.ValidateUpload(receiptFile, AllowedReceiptTypes, MaxReceiptBytes);
            if (!receiptValid)
            {
                ModelState.AddModelError(string.Empty, receiptError);
                await PopulateDropdownsAsync();
                return View(dto);
            }
        }

        try
        {
            var currentUser = await _userManager.GetUserAsync(User);
            Expense? expense = null;

            // Expense entity + account balance mutations in one atomic transaction so
            // neither can commit without the other.
            await _unitOfWork.ExecuteInTransactionAsync(async () =>
            {
                expense = _mapper.Map<Expense>(dto);
                expense.ExpenseNumber = await GenerateExpenseNumberAsync();
                expense.CompanyId = currentUser!.CompanyId;
                expense.CreatedBy = currentUser.Email;

                await _unitOfWork.Expenses.AddAsync(expense);
                await _accountBalanceService.DebitAsync(expense.ExpenseAccountId, expense.Amount);
                await _accountBalanceService.CreditAsync(expense.PaymentAccountId, expense.Amount);
                await _unitOfWork.CompleteAsync();
            });

            // Receipt upload runs after the transaction commits so expense.Id is available
            // and the core financial record is already secured. A blob failure here leaves
            // the expense intact with correct balances — just no receipt attachment.
            if (receiptFile != null)
            {
                expense!.ReceiptFilePath = await UploadReceiptAsync(receiptFile, expense.Id, currentUser.CompanyId);
                await _unitOfWork.Expenses.UpdateAsync(expense);
                await _unitOfWork.CompleteAsync();
            }

            TempData["Success"] = $"Expense {expense!.ExpenseNumber} recorded.";
            return RedirectToAction(nameof(Details), new { id = expense.Id });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating expense");
            ModelState.AddModelError(string.Empty, "An error occurred while saving.");
            await PopulateDropdownsAsync();
            return View(dto);
        }
    }

    // ── Edit ─────────────────────────────────────────────────────────────────

    /// <summary>
    /// Returns the edit form for an existing expense.
    /// </summary>
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    public async Task<IActionResult> Edit(int? id)
    {
        if (id == null) return NotFound();
        var expense = await _unitOfWork.Expenses.GetByIdAsync(id.Value);
        if (expense == null) return NotFound();

        await PopulateDropdownsAsync();
        return View(_mapper.Map<EditExpenseDto>(expense));
    }

    /// <summary>
    /// Saves expense edits. Because account balances were already applied when the expense was
    /// created, the old balances must be reversed before applying the new ones to keep the ledger
    /// accurate: the old expense account is credited (reversing the original debit) and the old
    /// payment account is debited (reversing the original credit), then the new accounts are
    /// updated with the new amount. If the account or amount is unchanged the net effect is zero.
    /// The old receipt blob is deleted from storage before uploading the replacement to avoid
    /// orphaned files.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    public async Task<IActionResult> Edit(int id, EditExpenseDto dto, IFormFile? receiptFile)
    {
        if (id != dto.Id) return NotFound();

        if (!ModelState.IsValid)
        {
            await PopulateDropdownsAsync();
            return View(dto);
        }

        if (receiptFile != null)
        {
            var (receiptValid, _, receiptError) = BlobFileHelper.ValidateUpload(receiptFile, AllowedReceiptTypes, MaxReceiptBytes);
            if (!receiptValid)
            {
                ModelState.AddModelError(string.Empty, receiptError);
                await PopulateDropdownsAsync();
                return View(dto);
            }
        }

        try
        {
            var expense = await _unitOfWork.Expenses.GetByIdAsync(id);
            if (expense == null) return NotFound();

            // Capture old values before overwriting
            var oldAmount = expense.Amount;
            var oldExpenseAccountId = expense.ExpenseAccountId;
            var oldPaymentAccountId = expense.PaymentAccountId;

            var currentUser = await _userManager.GetUserAsync(User);
            _mapper.Map(dto, expense);
            expense.UpdatedAt = DateTime.UtcNow;
            expense.UpdatedBy = currentUser?.Email;

            if (receiptFile != null)
            {
                // Delete old receipt if present
                if (!string.IsNullOrEmpty(expense.ReceiptFilePath))
                    await _blobStorage.DeleteAsync(_storageSettings.Containers.ReceiptImages, expense.ReceiptFilePath);

                expense.ReceiptFilePath = await UploadReceiptAsync(receiptFile, expense.Id, currentUser!.CompanyId);
            }

            // Reverse old balances, apply new balances
            await _accountBalanceService.CreditAsync(oldExpenseAccountId, oldAmount);
            await _accountBalanceService.DebitAsync(oldPaymentAccountId, oldAmount);
            await _accountBalanceService.DebitAsync(expense.ExpenseAccountId, expense.Amount);
            await _accountBalanceService.CreditAsync(expense.PaymentAccountId, expense.Amount);

            await _unitOfWork.Expenses.UpdateAsync(expense);
            await _unitOfWork.CompleteAsync();

            TempData["Success"] = $"Expense {expense.ExpenseNumber} updated.";
            return RedirectToAction(nameof(Details), new { id });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error updating expense {Id}", id);
            ModelState.AddModelError(string.Empty, "An error occurred while saving.");
            await PopulateDropdownsAsync();
            return View(dto);
        }
    }

    // ── Details ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Displays the read-only expense detail view, including vendor, expense account, payment
    /// account, and linked job (if cost was allocated to a job).
    /// </summary>
    public async Task<IActionResult> Details(int? id)
    {
        if (id == null) return NotFound();

        var expense = await _unitOfWork.Expenses.GetByIdAsync(
            id.Value, false, e => e.Vendor, e => e.ExpenseAccount, e => e.PaymentAccount, e => e.Job);

        if (expense == null) return NotFound();
        return View(_mapper.Map<ExpenseDto>(expense));
    }

    // ── Delete ───────────────────────────────────────────────────────────────

    /// <summary>
    /// Soft-deletes an expense and reverses its account-balance effects: the expense account is
    /// credited (reversing the original debit) and the payment account is debited (reversing the
    /// original credit). The receipt blob is permanently deleted from Azure Blob Storage because
    /// it is no longer referenced by any active record.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    public async Task<IActionResult> Delete(int id)
    {
        var expense = await _unitOfWork.Expenses.GetByIdAsync(id);
        if (expense != null)
        {
            if (!string.IsNullOrEmpty(expense.ReceiptFilePath))
                await _blobStorage.DeleteAsync(_storageSettings.Containers.ReceiptImages, expense.ReceiptFilePath);

            // Reverse account balances
            await _accountBalanceService.CreditAsync(expense.ExpenseAccountId, expense.Amount);
            await _accountBalanceService.DebitAsync(expense.PaymentAccountId, expense.Amount);
        }

        await _unitOfWork.Expenses.SoftDeleteAsync(id);
        await _unitOfWork.CompleteAsync();
        TempData["Success"] = "Expense deleted.";
        return RedirectToAction(nameof(Index));
    }

    // ── Receipt ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Streams the receipt file from Azure Blob Storage. Images are served inline
    /// (<c>Content-Disposition: inline</c>) so the browser can preview them in a tab; PDFs are
    /// served as downloads (<c>Content-Disposition: attachment</c>) because inline PDF rendering
    /// varies between browsers and can cause a poor UX.
    /// </summary>
    public async Task<IActionResult> ViewReceipt(int id)
    {
        var expense = await _unitOfWork.Expenses.GetByIdAsync(id);
        if (expense == null || string.IsNullOrEmpty(expense.ReceiptFilePath))
            return NotFound();

        var result = await _blobStorage.DownloadAsync(_storageSettings.Containers.ReceiptImages, expense.ReceiptFilePath);
        if (!result.Success) return NotFound();

        // Inline for images so the browser previews them; attachment for PDFs triggers download
        var ext = Path.GetExtension(expense.ReceiptFilePath).ToLowerInvariant();
        var contentType = result.ContentType.Length > 0 ? result.ContentType : BlobFileHelper.GetContentType(ext);
        var filename = $"Receipt-{expense.ExpenseNumber}{ext}";

        Response.Headers["Content-Disposition"] = ext == ".pdf"
            ? $"attachment; filename=\"{filename}\""
            : $"inline; filename=\"{filename}\"";

        return File(result.Content, contentType);
    }

    /// <summary>
    /// Removes the receipt attachment from an expense without deleting the expense itself. The
    /// blob is permanently deleted from Azure Blob Storage and the database path is nulled in the
    /// same save operation to keep them in sync.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    public async Task<IActionResult> DeleteReceipt(int id)
    {
        var expense = await _unitOfWork.Expenses.GetByIdAsync(id);
        if (expense == null) return NotFound();

        if (!string.IsNullOrEmpty(expense.ReceiptFilePath))
        {
            await _blobStorage.DeleteAsync(_storageSettings.Containers.ReceiptImages, expense.ReceiptFilePath);
            expense.ReceiptFilePath = null;
            expense.UpdatedAt = DateTime.UtcNow;
            expense.UpdatedBy = (await _userManager.GetUserAsync(User))?.Email;
            await _unitOfWork.Expenses.UpdateAsync(expense);
            await _unitOfWork.CompleteAsync();
        }

        TempData["Success"] = "Receipt removed.";
        return RedirectToAction(nameof(Details), new { id });
    }

    // ── Helpers ──────────────────────────────────────────────────────────────

    /// <summary>
    /// Loads all dropdowns required by the Create and Edit expense views: expense accounts
    /// (Expense and CostOfGoods types), payment accounts (Checking, Savings, CreditCard sub-types),
    /// active vendors, open jobs (for cost allocation), and payment methods. A single
    /// <c>FindAsync</c> fetches all accounts and then in-memory LINQ filters split them into the
    /// relevant subsets to avoid multiple database round trips.
    /// </summary>
    private async Task PopulateDropdownsAsync()
    {
        var dd = await AccountingDropdownHelper.LoadAsync(_unitOfWork);
        ViewBag.ExpenseAccounts = dd.ExpenseAccounts;
        ViewBag.PaymentAccounts = dd.BankAccounts;
        ViewBag.Vendors = dd.Vendors;
        ViewBag.Jobs = dd.ActiveJobs;
        ViewBag.PaymentMethods = dd.PaymentMethods;
    }

    /// <summary>
    /// Generates a sequential expense number in the format <c>EXP-YYMM-####</c>. Uses
    /// <c>IgnoreQueryFilters()</c> so that soft-deleted expense records are included in the
    /// max-sequence scan, preventing number reuse after deletion.
    /// </summary>
    private async Task<string> GenerateExpenseNumberAsync()
    {
        var prefix = $"EXP-{DateTime.Now:yyMM}-";
        var last = (await _unitOfWork.Expenses.FindAsync(
                e => e.ExpenseNumber.StartsWith(prefix), ignoreQueryFilters: true))
            .OrderByDescending(e => e.ExpenseNumber)
            .Select(e => e.ExpenseNumber)
            .FirstOrDefault();

        int next = 1;
        if (last != null && int.TryParse(last[prefix.Length..], out int num))
            next = num + 1;

        return $"{prefix}{next:D4}";
    }

    /// <summary>
    /// Uploads a receipt file to Azure Blob Storage at
    /// <c>{companyId}/expense-receipts/{expenseId}{ext}</c>. Returns the blob name (relative
    /// path) on success or <c>null</c> on failure, allowing the caller to continue saving the
    /// expense even when storage is unavailable.
    /// </summary>
    private async Task<string?> UploadReceiptAsync(IFormFile file, int expenseId, int companyId)
    {
        var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
        var blobName = $"{companyId}/expense-receipts/{expenseId}{ext}";
        using var stream = file.OpenReadStream();
        var result = await _blobStorage.UploadAsync(_storageSettings.Containers.ReceiptImages, blobName, stream, BlobFileHelper.GetContentType(ext));
        if (!result.Success)
        {
            _logger.LogError("Receipt upload failed for expense {Id}: {Error}", expenseId, result.ErrorMessage);
            return null;
        }
        return blobName;
    }



    // ── AI: Account Suggestion ────────────────────────────────────────────────

    /// <summary>
    /// AI-powered account categorisation for a single expense entry. If the caller does not
    /// supply <c>AvailableAccounts</c>, the controller fetches the active Expense and CostOfGoods
    /// accounts and merges them into the request before forwarding to
    /// <see cref="IAccountingAiService.SuggestAccountAsync"/>. Called on blur from the expense
    /// account dropdown when the user types a memo, helping reduce mis-categorisation. Rate-limited
    /// to the <c>Ai</c> policy to control Anthropic API usage.
    /// </summary>
    [HttpPost]
    [Authorize(Policy = AppConstants.Policies.CanManageInventory)]
    [EnableRateLimiting(AppConstants.RateLimitPolicies.Ai)]
    public async Task<IActionResult> SuggestAccount([FromBody] AccountSuggestionRequest request)
    {
        if (request == null)
            return Json(new { success = false, error = "Invalid request." });

        if (!request.AvailableAccounts.Any())
        {
            var currentUser = await _userManager.GetUserAsync(User);
            var allAccounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == currentUser!.CompanyId && a.IsActive);
            request.AvailableAccounts = allAccounts
                .Where(a => a.AccountType == AccountType.Expense ||
                            a.AccountType == AccountType.CostOfGoods)
                .Select(a => new AccountSummary
                {
                    Id = a.Id,
                    AccountNumber = a.AccountNumber,
                    Name = a.Name,
                    AccountType = a.AccountType.ToString(),
                    AccountSubType = a.AccountSubType.ToString()
                })
                .ToList();
        }

        var result = await _accountingAi.SuggestAccountAsync(request);
        var companyId = int.TryParse(User.FindFirst("CompanyId")?.Value, out var cid) ? cid : 0;
        var userId = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? "";
        await _usageLogger.LogAsync(companyId, userId, AppConstants.AiFeatures.AccountSuggest, inputLength: (request.Description?.Length ?? 0));
        return Json(result);
    }
}
