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

namespace SolutionErp.Application.Forms;

public record ContractTemplateDto(
    Guid Id,
    string FormCode,
    string Name,
    ContractType? ContractType,
    string FileName,
    string Format,
    string? FieldSpec,
    string? Description,
    bool IsActive);

// ========== List templates ==========
public record ListContractTemplatesQuery(ContractType? ContractType = null, bool OnlyActive = true)
    : IRequest<List<ContractTemplateDto>>;

public class ListContractTemplatesQueryHandler(IApplicationDbContext db)
    : IRequestHandler<ListContractTemplatesQuery, List<ContractTemplateDto>>
{
    public async Task<List<ContractTemplateDto>> Handle(ListContractTemplatesQuery request, CancellationToken ct)
    {
        var query = db.ContractTemplates.AsNoTracking();
        if (request.OnlyActive) query = query.Where(x => x.IsActive);
        if (request.ContractType is not null) query = query.Where(x => x.ContractType == request.ContractType);

        return await query
            .OrderBy(x => x.FormCode)
            .Select(x => new ContractTemplateDto(
                x.Id, x.FormCode, x.Name, x.ContractType, x.FileName, x.Format, x.FieldSpec, x.Description, x.IsActive))
            .ToListAsync(ct);
    }
}

// ========== Get one ==========
public record GetContractTemplateQuery(Guid Id) : IRequest<ContractTemplateDto>;

public class GetContractTemplateQueryHandler(IApplicationDbContext db)
    : IRequestHandler<GetContractTemplateQuery, ContractTemplateDto>
{
    public async Task<ContractTemplateDto> Handle(GetContractTemplateQuery request, CancellationToken ct)
    {
        var x = await db.ContractTemplates.AsNoTracking().FirstOrDefaultAsync(t => t.Id == request.Id, ct)
            ?? throw new NotFoundException("ContractTemplate", request.Id);
        return new ContractTemplateDto(
            x.Id, x.FormCode, x.Name, x.ContractType, x.FileName, x.Format, x.FieldSpec, x.Description, x.IsActive);
    }
}

// ========== Render ==========
public record RenderTemplateCommand(Guid TemplateId, Dictionary<string, string?> Data)
    : IRequest<RenderResult>;

public class RenderTemplateCommandValidator : AbstractValidator<RenderTemplateCommand>
{
    public RenderTemplateCommandValidator()
    {
        RuleFor(x => x.TemplateId).NotEmpty();
        RuleFor(x => x.Data).NotNull();
    }
}

public class RenderTemplateCommandHandler(
    IApplicationDbContext db,
    IFormRenderer renderer,
    IWebHostEnvironmentLocator envLocator) : IRequestHandler<RenderTemplateCommand, RenderResult>
{
    public async Task<RenderResult> Handle(RenderTemplateCommand request, CancellationToken ct)
    {
        var tpl = await db.ContractTemplates.AsNoTracking()
            .FirstOrDefaultAsync(t => t.Id == request.TemplateId, ct)
            ?? throw new NotFoundException("ContractTemplate", request.TemplateId);

        var absPath = Path.Combine(envLocator.WebRootPath, tpl.StoragePath);
        if (!File.Exists(absPath))
            throw new NotFoundException($"File template không tồn tại: {tpl.StoragePath}");

        var outName = $"{tpl.FormCode}_preview.{tpl.Format}";
        return await renderer.RenderAsync(absPath, tpl.Format, request.Data, outName, ct);
    }
}

// Abstraction nhỏ cho webRootPath — để Application không phụ thuộc IWebHostEnvironment
public interface IWebHostEnvironmentLocator
{
    string WebRootPath { get; }
    string ContentRootPath { get; }
}

// ========== EXPORT rendered template as PDF ==========
// Renders the same docx/xlsx as RenderTemplateCommand, then pipes through
// IPdfConverter (LibreOffice headless) to get a PDF. Accepts the SAME data
// dictionary so callers can preview PDF without duplicating logic.

public record ExportTemplatePdfCommand(Guid TemplateId, Dictionary<string, string?> Data)
    : IRequest<RenderResult>;

public class ExportTemplatePdfCommandHandler(
    IApplicationDbContext db,
    IFormRenderer renderer,
    IDocumentConverter converter,
    IWebHostEnvironmentLocator envLocator) : IRequestHandler<ExportTemplatePdfCommand, RenderResult>
{
    public async Task<RenderResult> Handle(ExportTemplatePdfCommand request, CancellationToken ct)
    {
        var tpl = await db.ContractTemplates.AsNoTracking()
            .FirstOrDefaultAsync(t => t.Id == request.TemplateId, ct)
            ?? throw new NotFoundException("ContractTemplate", request.TemplateId);

        var absPath = Path.Combine(envLocator.WebRootPath, tpl.StoragePath);
        if (!File.Exists(absPath))
            throw new NotFoundException($"File template không tồn tại: {tpl.StoragePath}");

        var rendered = await renderer.RenderAsync(absPath, tpl.Format, request.Data, "source", ct);
        var pdfBytes = await converter.ConvertAsync(rendered.Content, tpl.Format, "pdf", ct);
        var outName = $"{tpl.FormCode}.pdf";
        return new RenderResult(pdfBytes, outName, "application/pdf");
    }
}

// ========== UPLOAD new template ==========

public record UploadContractTemplateCommand(
    string FormCode,
    string Name,
    ContractType? ContractType,
    string? Description,
    string? FieldSpec,
    string FileName,
    string ContentType,
    long FileSize,
    Stream Content) : IRequest<ContractTemplateDto>;

public class UploadContractTemplateCommandValidator : AbstractValidator<UploadContractTemplateCommand>
{
    private const long MaxBytes = 10L * 1024 * 1024;
    // .doc/.xls accepted — auto-converted to .docx/.xlsx during upload handling.
    private static readonly HashSet<string> AllowedFormats = new(StringComparer.OrdinalIgnoreCase)
    {
        ".docx", ".xlsx", ".doc", ".xls",
    };

    public UploadContractTemplateCommandValidator()
    {
        RuleFor(x => x.FormCode).NotEmpty().MaximumLength(100)
            .Matches("^[A-Za-z0-9._-]+$")
            .WithMessage("FormCode chỉ dùng chữ, số, và các ký tự . _ -");
        RuleFor(x => x.Name).NotEmpty().MaximumLength(300);
        RuleFor(x => x.Description).MaximumLength(1000);
        RuleFor(x => x.FileSize).GreaterThan(0).LessThanOrEqualTo(MaxBytes)
            .WithMessage("Template tối đa 10 MB.");
        RuleFor(x => x.FileName).NotEmpty()
            .Must(name => AllowedFormats.Contains(Path.GetExtension(name)))
            .WithMessage("Chỉ chấp nhận file .docx/.xlsx (.doc/.xls sẽ tự convert).");
        RuleFor(x => x.FieldSpec).Must(BeValidJsonOrNull)
            .WithMessage("FieldSpec phải là JSON hợp lệ (hoặc để trống).");
    }

    private static bool BeValidJsonOrNull(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return true;
        try { System.Text.Json.JsonDocument.Parse(json); return true; }
        catch { return false; }
    }
}

public class UploadContractTemplateCommandHandler(
    IApplicationDbContext db,
    IDocumentConverter converter,
    IWebHostEnvironmentLocator env) : IRequestHandler<UploadContractTemplateCommand, ContractTemplateDto>
{
    public async Task<ContractTemplateDto> Handle(UploadContractTemplateCommand request, CancellationToken ct)
    {
        // Enforce unique FormCode — admin-friendly error via ConflictException
        if (await db.ContractTemplates.AnyAsync(t => t.FormCode == request.FormCode, ct))
            throw new ConflictException($"FormCode '{request.FormCode}' đã tồn tại.");

        var srcExt = Path.GetExtension(request.FileName).TrimStart('.').ToLowerInvariant();

        // Auto-convert legacy .doc/.xls → .docx/.xlsx so rendering pipeline
        // (DocxRenderer / XlsxRenderer) works uniformly. Admin sees the upload
        // succeed with the new format without extra steps.
        var (finalBytes, finalExt) = (srcExt is "doc" or "xls")
            ? await ReadAndConvert(request.Content, srcExt, ct)
            : (await ReadAllBytes(request.Content, ct), srcExt);

        var id = Guid.NewGuid();
        var safeFile = $"{request.FormCode}_{id:N}.{finalExt}";
        var templatesDir = Path.Combine(env.WebRootPath, "templates");
        Directory.CreateDirectory(templatesDir);
        var absPath = Path.Combine(templatesDir, safeFile);

        await File.WriteAllBytesAsync(absPath, finalBytes, ct);

        // Preserve original filename for user display but reflect final format.
        var displayName = srcExt != finalExt
            ? Path.ChangeExtension(request.FileName, finalExt)
            : request.FileName;

        var entity = new Domain.Forms.ContractTemplate
        {
            Id = id,
            FormCode = request.FormCode,
            Name = request.Name,
            ContractType = request.ContractType,
            FileName = displayName,
            StoragePath = $"templates/{safeFile}",
            Format = finalExt,
            FieldSpec = request.FieldSpec,
            Description = request.Description,
            IsActive = true,
        };
        db.ContractTemplates.Add(entity);
        await db.SaveChangesAsync(ct);

        return new ContractTemplateDto(
            entity.Id, entity.FormCode, entity.Name, entity.ContractType,
            entity.FileName, entity.Format, entity.FieldSpec, entity.Description, entity.IsActive);
    }

    private static async Task<byte[]> ReadAllBytes(Stream s, CancellationToken ct)
    {
        using var ms = new MemoryStream();
        await s.CopyToAsync(ms, ct);
        return ms.ToArray();
    }

    private async Task<(byte[] Bytes, string Ext)> ReadAndConvert(Stream s, string srcExt, CancellationToken ct)
    {
        var srcBytes = await ReadAllBytes(s, ct);
        var targetExt = srcExt switch { "doc" => "docx", "xls" => "xlsx", _ => srcExt };
        var converted = await converter.ConvertAsync(srcBytes, srcExt, targetExt, ct);
        return (converted, targetExt);
    }
}

// ========== UPDATE metadata + FieldSpec ==========

public record UpdateContractTemplateCommand(
    Guid Id,
    string Name,
    ContractType? ContractType,
    string? Description,
    string? FieldSpec,
    bool IsActive) : IRequest;

public class UpdateContractTemplateCommandValidator : AbstractValidator<UpdateContractTemplateCommand>
{
    public UpdateContractTemplateCommandValidator()
    {
        RuleFor(x => x.Id).NotEmpty();
        RuleFor(x => x.Name).NotEmpty().MaximumLength(300);
        RuleFor(x => x.Description).MaximumLength(1000);
        RuleFor(x => x.FieldSpec).Must(BeValidJsonOrNull)
            .WithMessage("FieldSpec phải là JSON hợp lệ (hoặc để trống).");
    }

    private static bool BeValidJsonOrNull(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return true;
        try { System.Text.Json.JsonDocument.Parse(json); return true; }
        catch { return false; }
    }
}

public class UpdateContractTemplateCommandHandler(IApplicationDbContext db)
    : IRequestHandler<UpdateContractTemplateCommand>
{
    public async Task Handle(UpdateContractTemplateCommand request, CancellationToken ct)
    {
        var entity = await db.ContractTemplates.FirstOrDefaultAsync(t => t.Id == request.Id, ct)
            ?? throw new NotFoundException("ContractTemplate", request.Id);

        entity.Name = request.Name;
        entity.ContractType = request.ContractType;
        entity.Description = request.Description;
        entity.FieldSpec = request.FieldSpec;
        entity.IsActive = request.IsActive;

        await db.SaveChangesAsync(ct);
    }
}

// ========== DELETE (soft via IsActive=false) ==========

public record DeleteContractTemplateCommand(Guid Id) : IRequest;

public class DeleteContractTemplateCommandHandler(IApplicationDbContext db)
    : IRequestHandler<DeleteContractTemplateCommand>
{
    public async Task Handle(DeleteContractTemplateCommand request, CancellationToken ct)
    {
        var entity = await db.ContractTemplates.FirstOrDefaultAsync(t => t.Id == request.Id, ct)
            ?? throw new NotFoundException("ContractTemplate", request.Id);

        // Soft delete — AuditableEntity query filter already handles IsDeleted.
        // But simpler MVP: just flip IsActive so historical contracts referencing
        // this template can still resolve it.
        entity.IsActive = false;
        await db.SaveChangesAsync(ct);
    }
}
