using FluentValidation;
using MediatR;
using Microsoft.EntityFrameworkCore;
using SolutionErp.Application.Common.Exceptions;
using SolutionErp.Application.Common.Interfaces;
using SolutionErp.Application.Contracts.Dtos;
using SolutionErp.Domain.Contracts;

namespace SolutionErp.Application.Contracts;

// ========== UPLOAD ==========
// File payload is decoupled from the command so MediatR-style CQRS works with
// multipart/form-data: the controller reads IFormFile, hands the Application
// layer a bare Stream + metadata. Keeps Application free of ASP.NET types.

public record UploadContractAttachmentCommand(
    Guid ContractId,
    string FileName,
    string ContentType,
    long FileSize,
    Stream Content,
    AttachmentPurpose Purpose,
    string? Note) : IRequest<ContractAttachmentDto>;

public class UploadContractAttachmentCommandValidator : AbstractValidator<UploadContractAttachmentCommand>
{
    // 20 MB default — configurable later via Uploads:MaxFileSize if needed
    private const long MaxBytes = 20L * 1024 * 1024;

    // MIME whitelist — contract scans (pdf/image) + editable sources (word/excel)
    private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        "application/pdf",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document", // .docx
        "application/msword",                                                      // .doc
        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",       // .xlsx
        "application/vnd.ms-excel",                                                // .xls
        "image/png",
        "image/jpeg",
        "image/webp",
    };

    public UploadContractAttachmentCommandValidator()
    {
        RuleFor(x => x.ContractId).NotEmpty();
        RuleFor(x => x.FileName).NotEmpty().MaximumLength(255);
        RuleFor(x => x.FileSize).GreaterThan(0).LessThanOrEqualTo(MaxBytes)
            .WithMessage("File vượt quá 20 MB.");
        RuleFor(x => x.ContentType).Must(c => AllowedContentTypes.Contains(c))
            .WithMessage("Định dạng file không được hỗ trợ. Cho phép: pdf, doc(x), xls(x), png, jpg, webp.");
        RuleFor(x => x.Purpose).IsInEnum();
        RuleFor(x => x.Note).MaximumLength(500);
    }
}

public class UploadContractAttachmentCommandHandler(
    IApplicationDbContext db,
    IFileStorage storage,
    IChangelogService changelog) : IRequestHandler<UploadContractAttachmentCommand, ContractAttachmentDto>
{
    public async Task<ContractAttachmentDto> Handle(UploadContractAttachmentCommand request, CancellationToken ct)
    {
        var contract = await db.Contracts.FirstOrDefaultAsync(c => c.Id == request.ContractId, ct)
            ?? throw new NotFoundException("Contract", request.ContractId);

        var attId = Guid.NewGuid();
        var safeName = SanitizeFileName(request.FileName);
        var relativePath = $"contracts/{contract.Id}/{attId}_{safeName}";

        await storage.SaveAsync(relativePath, request.Content, ct);

        var entity = new ContractAttachment
        {
            Id = attId,
            ContractId = contract.Id,
            FileName = request.FileName,
            StoragePath = relativePath,
            FileSize = request.FileSize,
            ContentType = request.ContentType,
            Purpose = request.Purpose,
            Note = request.Note,
        };
        db.ContractAttachments.Add(entity);
        await changelog.LogAttachmentAsync(contract.Id, entity.Id, ChangelogAction.Insert, entity.FileName, contract.Phase, ct);
        await db.SaveChangesAsync(ct);

        return new ContractAttachmentDto(
            entity.Id, entity.FileName, entity.StoragePath, entity.FileSize,
            entity.ContentType, entity.Purpose, entity.Note, DateTime.UtcNow);
    }

    private static string SanitizeFileName(string name)
    {
        // Strip path components + replace chars that are invalid on filesystems
        var baseName = Path.GetFileName(name);
        foreach (var c in Path.GetInvalidFileNameChars())
            baseName = baseName.Replace(c, '_');
        // Also strip leading dots (hidden files) and collapse whitespace
        baseName = baseName.TrimStart('.');
        if (string.IsNullOrWhiteSpace(baseName)) baseName = "file";
        return baseName.Length > 200 ? baseName[^200..] : baseName;
    }
}

// ========== DOWNLOAD ==========

public record DownloadContractAttachmentQuery(Guid ContractId, Guid AttachmentId) : IRequest<ContractAttachmentFile>;

public record ContractAttachmentFile(Stream Content, string FileName, string ContentType);

public class DownloadContractAttachmentQueryHandler(
    IApplicationDbContext db,
    IFileStorage storage) : IRequestHandler<DownloadContractAttachmentQuery, ContractAttachmentFile>
{
    public async Task<ContractAttachmentFile> Handle(DownloadContractAttachmentQuery request, CancellationToken ct)
    {
        var att = await db.ContractAttachments.AsNoTracking()
            .FirstOrDefaultAsync(a => a.Id == request.AttachmentId && a.ContractId == request.ContractId, ct)
            ?? throw new NotFoundException("Attachment", request.AttachmentId);

        var stream = await storage.OpenReadAsync(att.StoragePath, ct);
        return new ContractAttachmentFile(stream, att.FileName, att.ContentType);
    }
}

// ========== DELETE ==========

public record DeleteContractAttachmentCommand(Guid ContractId, Guid AttachmentId) : IRequest;

public class DeleteContractAttachmentCommandHandler(
    IApplicationDbContext db,
    IFileStorage storage,
    IChangelogService changelog) : IRequestHandler<DeleteContractAttachmentCommand>
{
    public async Task Handle(DeleteContractAttachmentCommand request, CancellationToken ct)
    {
        var att = await db.ContractAttachments
            .FirstOrDefaultAsync(a => a.Id == request.AttachmentId && a.ContractId == request.ContractId, ct)
            ?? throw new NotFoundException("Attachment", request.AttachmentId);
        var contract = await db.Contracts.AsNoTracking()
            .FirstOrDefaultAsync(c => c.Id == request.ContractId, ct);

        db.ContractAttachments.Remove(att);
        await changelog.LogAttachmentAsync(request.ContractId, att.Id, ChangelogAction.Delete, att.FileName, contract?.Phase ?? ContractPhase.DangSoanThao, ct);
        await db.SaveChangesAsync(ct);

        // Best-effort file delete — if it fails, DB row is already gone (orphan
        // cleanup job can sweep storage later). Don't block API response.
        try { await storage.DeleteAsync(att.StoragePath, ct); }
        catch { /* ignore */ }
    }
}
