using ERPCore.Domain.Entities;
using ERPCore.Domain.Enums;
using ERPCore.Dtos.Hrm;
using ERPCore.Infra.Storage;
using ERPCore.Infra.UoW;
using ERPCore.Repositories.Interfaces;
using ERPCore.System.Errors;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace ERPCore.Services.Hrm;

/// <summary>
/// Uploaded staff document service (FR-HR-DOC-02..04). Extension allowlist + declared
/// content-type cross-check + size cap are enforced here, server-authoritative
/// (docs/12-BACKEND-HRM.md §B.5, 02-SECURITY C.8) — magic-byte sniffing / antivirus
/// scanning are an explicitly deferred accepted risk, not silently skipped.
/// </summary>
public sealed class EmployeeDocumentService : Services.Interfaces.IEmployeeDocumentService
{
    private static readonly Dictionary<string, string> AllowedExtensionContentTypes = new(StringComparer.OrdinalIgnoreCase)
    {
        [".pdf"] = "application/pdf",
        [".jpg"] = "image/jpeg",
        [".jpeg"] = "image/jpeg",
        [".png"] = "image/png",
        [".docx"] = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
    };

    private readonly IRepository<EmployeeDocument> _documents;
    private readonly IRepository<Employee> _employees;
    private readonly IRepository<HrDocumentType> _documentTypes;
    private readonly IFileStorageService _storage;
    private readonly IUnitOfWork _uow;
    private readonly long _maxSizeBytes;

    public EmployeeDocumentService(
        IRepository<EmployeeDocument> documents, IRepository<Employee> employees, IRepository<HrDocumentType> documentTypes,
        IFileStorageService storage, IUnitOfWork uow, IConfiguration configuration)
    {
        _documents = documents;
        _employees = employees;
        _documentTypes = documentTypes;
        _storage = storage;
        _uow = uow;
        _maxSizeBytes = configuration.GetValue<long?>("FileStorage:MaxSizeBytes") ?? 10 * 1024 * 1024;
    }

    public async Task<List<EmployeeDocumentDto>> ListAsync(int employeeId, CancellationToken ct = default)
    {
        return await _documents.Query().AsNoTracking()
            .Include(d => d.HrDocumentType)
            .Where(d => d.EmployeeId == employeeId)
            .Select(d => Map(d))
            .ToListAsync(ct);
    }

    public async Task<EmployeeDocumentDto> UploadAsync(
        int employeeId, UploadEmployeeDocumentRequest request, Stream fileContent, string fileName, string contentType,
        int actorUserId, CancellationToken ct = default)
    {
        if (!await _employees.Query().AnyAsync(e => e.EmployeeId == employeeId, ct))
            throw new NotFoundException($"Employee {employeeId} was not found.");

        var docType = await _documentTypes.GetByIdAsync(request.HrDocumentTypeId, ct)
            ?? throw new NotFoundException($"Document type {request.HrDocumentTypeId} was not found.");

        var extension = Path.GetExtension(fileName);
        if (!AllowedExtensionContentTypes.TryGetValue(extension, out var expectedContentType)
            || !string.Equals(expectedContentType, contentType, StringComparison.OrdinalIgnoreCase))
            throw new DomainException(ErrorCodes.FileTypeNotAllowed,
                $"File type '{extension}'/'{contentType}' is not allowed.", 422);

        if (fileContent.Length > _maxSizeBytes)
            throw new DomainException(ErrorCodes.FileTooLarge,
                $"File exceeds the maximum allowed size of {_maxSizeBytes} bytes.", 413);

        var (storedFileName, relativePath, sizeBytes) = await _storage.SaveAsync(fileContent, fileName, contentType, ct);

        var document = new EmployeeDocument
        {
            EmployeeId = employeeId,
            HrDocumentTypeId = docType.HrDocumentTypeId,
            OriginalFileName = fileName,
            StoredFileName = storedFileName,
            RelativePath = relativePath,
            ContentType = contentType,
            SizeBytes = sizeBytes,
            IssueDate = request.IssueDate,
            ExpiryDate = request.ExpiryDate,
            Notes = request.Notes?.Trim(),
            UploadedBy = actorUserId,
            UploadedAt = DateTime.UtcNow,
            Status = EmployeeDocumentStatus.Active
        };

        await _documents.AddAsync(document, ct);
        await _uow.SaveChangesAsync(ct);

        document.HrDocumentType = docType;
        return Map(document);
    }

    public async Task<(Stream Content, string FileName, string ContentType)> DownloadAsync(
        int employeeId, int documentId, CancellationToken ct = default)
    {
        var document = await _documents.Query().AsNoTracking()
            .FirstOrDefaultAsync(d => d.EmployeeDocumentId == documentId && d.EmployeeId == employeeId, ct)
            ?? throw new NotFoundException($"Document {documentId} was not found for employee {employeeId}.");

        var stream = await _storage.OpenReadAsync(document.RelativePath, ct);
        return (stream, document.OriginalFileName, document.ContentType);
    }

    public async Task SetStatusAsync(int employeeId, int documentId, EmployeeDocumentStatus status, CancellationToken ct = default)
    {
        var document = await _documents.Query()
            .FirstOrDefaultAsync(d => d.EmployeeDocumentId == documentId && d.EmployeeId == employeeId, ct)
            ?? throw new NotFoundException($"Document {documentId} was not found for employee {employeeId}.");

        document.Status = status;
        await _uow.SaveChangesAsync(ct);
    }

    private static EmployeeDocumentDto Map(EmployeeDocument d) => new(
        d.EmployeeDocumentId, d.EmployeeId, d.HrDocumentTypeId, d.HrDocumentType?.Name,
        d.OriginalFileName, d.ContentType, d.SizeBytes, d.IssueDate, d.ExpiryDate, d.Notes,
        d.UploadedBy, d.UploadedAt, d.Status);
}
