using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using Books.Api.AiBookkeeper;
using Books.Api.Authorization;
using Books.Api.Commands.Attachments;
using Books.Api.Commands.JournalEntryDrafts;
using Books.Api.Domain.Attachments;
using Books.Api.Domain.JournalEntryDrafts;
using Books.Api.EventFlow.Repositories;
using Books.Api.Infrastructure.FileStorage;
using EventFlow;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Books.Api.Controllers;

/// <summary>
/// REST API for AI-powered document processing.
/// Handles document upload, AI analysis, draft creation, and bank transaction matching.
/// </summary>
[ApiController]
[Route("api/documents")]
[Authorize]
public class DocumentProcessingController(
    ICommandBus commandBus,
    IAiBookkeeperClient aiClient,
    IChartOfAccountsProvider chartProvider,
    IAccountMappingService accountMapping,
    IBankTransactionMatcher transactionMatcher,
    IDocumentHashRepository hashRepository,
    IBankTransactionRepository bankTransactionRepository,
    IVoucherNumberService voucherNumberService,
    IFileStorageService fileStorage,
    ICompanyAccessService companyAccess,
    IWebHostEnvironment environment,
    ILogger<DocumentProcessingController> logger) : ControllerBase
{
    private const long MaxFileSize = 10 * 1024 * 1024; // 10MB
    private static readonly HashSet<string> AllowedContentTypes =
    [
        "application/pdf",
        "image/png",
        "image/jpeg",
        "image/jpg",
        "image/gif"
    ];

    /// <summary>
    /// Process a document using AI and optionally match to a bank transaction.
    /// </summary>
    /// <param name="companyId">Company ID</param>
    /// <param name="document">Document file (PDF, PNG, JPG)</param>
    /// <param name="cancellationToken">Cancellation token</param>
    [HttpPost("process")]
    [RequestSizeLimit(MaxFileSize)]
    public async Task<IActionResult> ProcessDocument(
        [FromQuery] string companyId,
        [FromForm] IFormFile document,
        CancellationToken cancellationToken)
    {
        // Validate authentication
        var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
        if (string.IsNullOrEmpty(userId))
        {
            return Unauthorized(new DocumentProcessingError("NOT_AUTHENTICATED", "Du skal være logget ind"));
        }

        // Validate company access
        var canWrite = await companyAccess.CanWriteAsync(companyId, cancellationToken);
        if (!canWrite)
        {
            return Forbid();
        }

        // Validate file
        if (document.Length == 0)
        {
            return BadRequest(new DocumentProcessingError("NO_FILE", "Ingen fil uploadet"));
        }

        if (document.Length > MaxFileSize)
        {
            return BadRequest(new DocumentProcessingError("FILE_TOO_LARGE", "Filen er for stor (maks 10MB)"));
        }

        if (!AllowedContentTypes.Contains(document.ContentType.ToLowerInvariant()))
        {
            return BadRequest(new DocumentProcessingError(
                "INVALID_FILE_TYPE",
                "Kun PDF og billeder (PNG, JPG) er tilladt"));
        }

        try
        {
            // 1. Calculate content hash
            string contentHash;
            await using (var hashStream = document.OpenReadStream())
            {
                contentHash = await ComputeHashAsync(hashStream, cancellationToken);
            }

            // 2. Check for duplicate
            var existingHash = await hashRepository.GetByHashAsync(companyId, contentHash, cancellationToken);
            if (existingHash != null)
            {
                logger.LogInformation(
                    "Duplicate document detected for company {CompanyId}: {Hash}",
                    companyId, contentHash);

                return Ok(new DocumentProcessingResult
                {
                    IsDuplicate = true,
                    DraftId = existingHash.DraftId,
                    AttachmentId = existingHash.AttachmentId,
                    Message = "Dokumentet er allerede behandlet"
                });
            }

            // 3. Get chart of accounts
            var chartOfAccounts = await chartProvider.GetChartOfAccountsAsync(companyId, cancellationToken);

            // 4. Call AI Bookkeeper
            AiBookkeeperResponse aiResponse;
            await using (var aiStream = document.OpenReadStream())
            {
                aiResponse = await aiClient.ProcessDocumentAsync(
                    aiStream,
                    document.FileName,
                    document.ContentType,
                    chartOfAccounts,
                    cancellationToken);
            }

            if (!aiResponse.Success)
            {
                logger.LogWarning(
                    "AI Bookkeeper failed for document {FileName}: {Error}",
                    document.FileName, aiResponse.ErrorMessage);

                return StatusCode(503, new DocumentProcessingError(
                    "AI_UNAVAILABLE",
                    aiResponse.ErrorMessage ?? "AI-tjenesten er midlertidigt utilgængelig"));
            }

            // 5. Store attachment
            string attachmentId;
            await using (var storageStream = document.OpenReadStream())
            {
                var storageResult = await fileStorage.StoreAsync(
                    companyId,
                    document.FileName,
                    document.ContentType,
                    storageStream,
                    cancellationToken);

                var attId = AttachmentId.New;
                attachmentId = attId.Value;

                var uploadCommand = new UploadAttachmentCommand(
                    attId,
                    companyId,
                    storageResult.StoredFileName,
                    document.FileName,
                    document.ContentType,
                    storageResult.FileSize,
                    storageResult.StoragePath,
                    userId,
                    null); // draftId will be set later

                await commandBus.PublishAsync(uploadCommand, cancellationToken);
            }

            // 6. Map standard accounts to company accounts
            List<MappedSuggestedLine>? mappedLines = null;
            if (aiResponse.Suggestion?.Lines != null)
            {
                mappedLines = await accountMapping.MapSuggestedLinesAsync(
                    companyId,
                    aiResponse.Suggestion.Lines,
                    cancellationToken);
            }

            // 7. Create JournalEntryDraft
            var draftId = JournalEntryDraftId.New();
            var voucherNumber = await voucherNumberService.GetNextVoucherNumberAsync(companyId, null, cancellationToken);

            // Serialize full extraction data to preserve all AI-extracted fields
            string? extractionDataJson = null;
            if (aiResponse.Extraction != null)
            {
                extractionDataJson = System.Text.Json.JsonSerializer.Serialize(aiResponse.Extraction,
                    new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase });
            }

            var draftName = aiResponse.Extraction?.Vendor ?? document.FileName;
            var createDraftCommand = new CreateJournalEntryDraftCommand(
                draftId,
                companyId,
                draftName,
                userId,
                voucherNumber,
                extractionDataJson);

            await commandBus.PublishAsync(createDraftCommand, cancellationToken);

            // Update draft with AI-suggested lines
            var draftLines = CreateDraftLines(mappedLines);
            var updateDraftCommand = new UpdateJournalEntryDraftCommand(
                draftId,
                draftName,
                aiResponse.Extraction?.Date,
                aiResponse.Extraction?.InvoiceNumber ?? aiResponse.Suggestion?.Description,
                null, // fiscalYearId - let the system determine
                draftLines,
                [attachmentId]);

            await commandBus.PublishAsync(updateDraftCommand, cancellationToken);

            // 8. Find matching bank transaction
            var matchedTransaction = await FindMatchingTransaction(
                companyId,
                aiResponse.Extraction?.TotalAmount,
                cancellationToken);

            // 9. Link draft to transaction if matched
            if (matchedTransaction != null)
            {
                await bankTransactionRepository.UpdateStatusAsync(
                    matchedTransaction.Id,
                    "booked",
                    draftId.Value,
                    cancellationToken);

                logger.LogInformation(
                    "Linked draft {DraftId} to bank transaction {TransactionId}",
                    draftId.Value, matchedTransaction.Id);
            }

            // 10. Save content hash
            await hashRepository.InsertAsync(
                companyId,
                contentHash,
                document.FileName,
                attachmentId,
                draftId.Value,
                cancellationToken);

            // 11. Build response
            var result = new DocumentProcessingResult
            {
                DraftId = draftId.Value,
                AttachmentId = attachmentId,
                IsDuplicate = false,
                Extraction = aiResponse.Extraction != null ? new ExtractionResult
                {
                    Vendor = aiResponse.Extraction.Vendor,
                    VendorCvr = aiResponse.Extraction.VendorCvr,
                    Amount = aiResponse.Extraction.TotalAmount,
                    AmountExVat = aiResponse.Extraction.AmountExVat,
                    VatAmount = aiResponse.Extraction.VatAmount,
                    Date = aiResponse.Extraction.Date?.ToString("yyyy-MM-dd"),
                    DueDate = aiResponse.Extraction.DueDate?.ToString("yyyy-MM-dd"),
                    InvoiceNumber = aiResponse.Extraction.InvoiceNumber,
                    DocumentType = aiResponse.Extraction.DocumentType,
                    Currency = aiResponse.Extraction.Currency,
                    PaymentReference = aiResponse.Extraction.PaymentReference,
                    LineItems = aiResponse.Extraction.LineItems?.Select(li => new ExtractedLineItemResult
                    {
                        Description = li.Description,
                        Quantity = li.Quantity,
                        UnitPrice = li.UnitPrice,
                        Amount = li.Amount,
                        VatRate = li.VatRate
                    }).ToList()
                } : null,
                AccountSuggestion = mappedLines != null && mappedLines.Any(l => l.IsMapped)
                    ? new AccountSuggestionResult
                    {
                        MappedAccountId = mappedLines.FirstOrDefault(l => l.IsMapped)?.MappedAccount?.Id,
                        MappedAccountNumber = mappedLines.FirstOrDefault(l => l.IsMapped)?.MappedAccount?.AccountNumber,
                        MappedAccountName = mappedLines.FirstOrDefault(l => l.IsMapped)?.MappedAccount?.Name,
                        Confidence = aiResponse.Suggestion?.Confidence ?? 0
                    }
                    : null,
                BankTransactionMatch = matchedTransaction != null
                    ? new BankTransactionMatchResult
                    {
                        TransactionId = matchedTransaction.Id,
                        Amount = matchedTransaction.Amount,
                        Date = matchedTransaction.TransactionDate.ToString("yyyy-MM-dd"),
                        Description = matchedTransaction.Description,
                        Counterparty = matchedTransaction.DisplayCounterparty
                    }
                    : null,
                SuggestedLines = mappedLines?.Select(ml => new SuggestedJournalLine
                {
                    AccountId = ml.MappedAccount?.Id,
                    AccountNumber = ml.MappedAccount?.AccountNumber,
                    AccountName = ml.MappedAccount?.Name ?? ml.Original.AccountName,
                    DebitAmount = ml.Original.DebitAmount,
                    CreditAmount = ml.Original.CreditAmount,
                    VatCode = ml.Original.VatCode
                }).ToList()
            };

            logger.LogInformation(
                "Successfully processed document {FileName} for company {CompanyId}. Draft: {DraftId}, Match: {HasMatch}",
                document.FileName, companyId, draftId.Value, matchedTransaction != null);

            return Ok(result);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Unexpected error processing document {FileName}", document.FileName);

            var errorMessage = environment.IsDevelopment()
                ? $"{ex.Message}\n\nStack trace:\n{ex.StackTrace}"
                : "Der opstod en uventet fejl ved behandling af dokumentet";

            return StatusCode(500, new DocumentProcessingError(
                "PROCESSING_FAILED",
                errorMessage));
        }
    }

    private static async Task<string> ComputeHashAsync(Stream stream, CancellationToken cancellationToken)
    {
        using var sha256 = SHA256.Create();
        var hashBytes = await sha256.ComputeHashAsync(stream, cancellationToken);
        return Convert.ToHexString(hashBytes).ToLowerInvariant();
    }

    private async Task<EventFlow.ReadModels.BankTransactionDto?> FindMatchingTransaction(
        string companyId,
        decimal? amount,
        CancellationToken cancellationToken)
    {
        if (!amount.HasValue || amount.Value == 0)
        {
            return null;
        }

        // For expenses (invoices to pay), the document amount is positive
        // The bank transaction will be negative (money leaving the account)
        // So we search for -amount
        return await transactionMatcher.FindMatchingTransactionAsync(
            companyId,
            -amount.Value, // Negate for expense matching
            0.01m,
            cancellationToken);
    }

    private static List<DraftLine> CreateDraftLines(List<MappedSuggestedLine>? mappedLines)
    {
        if (mappedLines == null || mappedLines.Count == 0)
        {
            return [];
        }

        var lineNumber = 1;
        var result = new List<DraftLine>();

        foreach (var mapped in mappedLines)
        {
            result.Add(new DraftLine(
                lineNumber++,
                mapped.MappedAccount?.Id,
                mapped.Original.DebitAmount,
                mapped.Original.CreditAmount,
                mapped.Original.AccountName,
                mapped.Original.VatCode));
        }

        return result;
    }
}

// Response DTOs

public class DocumentProcessingResult
{
    public string? DraftId { get; set; }
    public string? AttachmentId { get; set; }
    public bool IsDuplicate { get; set; }
    public string? Message { get; set; }
    public ExtractionResult? Extraction { get; set; }
    public AccountSuggestionResult? AccountSuggestion { get; set; }
    public BankTransactionMatchResult? BankTransactionMatch { get; set; }
    public List<SuggestedJournalLine>? SuggestedLines { get; set; }
}

public class SuggestedJournalLine
{
    public string? AccountId { get; set; }
    public string? AccountNumber { get; set; }
    public string? AccountName { get; set; }
    public decimal DebitAmount { get; set; }
    public decimal CreditAmount { get; set; }
    public string? VatCode { get; set; }
}

public class ExtractionResult
{
    public string? Vendor { get; set; }
    public string? VendorCvr { get; set; }
    public decimal? Amount { get; set; }
    public decimal? AmountExVat { get; set; }
    public decimal? VatAmount { get; set; }
    public string? Date { get; set; }
    public string? DueDate { get; set; }
    public string? InvoiceNumber { get; set; }
    public string? DocumentType { get; set; }
    public string? Currency { get; set; }
    public string? PaymentReference { get; set; }
    public List<ExtractedLineItemResult>? LineItems { get; set; }
}

public class ExtractedLineItemResult
{
    public string? Description { get; set; }
    public decimal? Quantity { get; set; }
    public decimal? UnitPrice { get; set; }
    public decimal? Amount { get; set; }
    public decimal? VatRate { get; set; }
}

public class AccountSuggestionResult
{
    public string? MappedAccountId { get; set; }
    public string? MappedAccountNumber { get; set; }
    public string? MappedAccountName { get; set; }
    public decimal Confidence { get; set; }
}

public class BankTransactionMatchResult
{
    public string? TransactionId { get; set; }
    public decimal Amount { get; set; }
    public string? Date { get; set; }
    public string? Description { get; set; }
    public string? Counterparty { get; set; }
}

public class DocumentProcessingError
{
    public string Code { get; set; }
    public string Message { get; set; }

    public DocumentProcessingError(string code, string message)
    {
        Code = code;
        Message = message;
    }
}
