using ERPCore.Domain.Entities;
using ERPCore.Infra.Auth;
using ERPCore.Infra.Persistence.Auditing;
using Microsoft.EntityFrameworkCore;

namespace ERPCore.Infra.Persistence;

/// <summary>
/// EF Core context for the ERP database. The 42 Phase 1 entities and their
/// <see cref="IEntityTypeConfiguration{TEntity}"/> configurations are added under
/// Domain/Entities and Infra/Persistence/Configurations as they are implemented.
/// The authoritative schema lives in docs/10-BACKEND-PHASE1.md — do not invent it here.
/// Every save writes an immutable audit trail (FR-X-02) via <see cref="AuditScribe"/>.
/// </summary>
public class ErpDbContext : DbContext
{
    private readonly ICurrentUser _currentUser;
    private bool _writingAuditLogs;

    public ErpDbContext(DbContextOptions<ErpDbContext> options, ICurrentUser currentUser) : base(options)
    {
        _currentUser = currentUser;
    }

    // --- Master Data (docs/10 Part C.1) ---
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Category> Categories => Set<Category>();
    public DbSet<SubCategory> SubCategories => Set<SubCategory>();
    public DbSet<Brand> Brands => Set<Brand>();
    /// <summary>Color/Size/Material dimension names. Unlinked to Item by design (docs/10 C.9).</summary>
    public DbSet<ItemType> ItemTypes => Set<ItemType>();
    public DbSet<Uom> Uoms => Set<Uom>();
    public DbSet<Item> Items => Set<Item>();
    public DbSet<ItemReorder> ItemReorders => Set<ItemReorder>();
    public DbSet<Vendor> Vendors => Set<Vendor>();
    public DbSet<Warehouse> Warehouses => Set<Warehouse>();
    public DbSet<Bin> Bins => Set<Bin>();
    
    /// <summary>Singleton row (FR-MD-11).</summary>
    public DbSet<ProductConfig> ProductConfig => Set<ProductConfig>();

    // --- Cross-cutting (docs/10 Part C.7) ---
    public DbSet<User> Users => Set<User>();
    public DbSet<NumberSequence> NumberSequences => Set<NumberSequence>();

    // --- RBAC / sidebar (docs/10 Part C.8) ---
    public DbSet<Role> Roles => Set<Role>();
    public DbSet<NavItem> NavItems => Set<NavItem>();
    public DbSet<SubNavItem> SubNavItems => Set<SubNavItem>();
    public DbSet<Permission> Permissions => Set<Permission>();
    public DbSet<RolePermission> RolePermissions => Set<RolePermission>();

    // --- Procurement (docs/10 Part C.2) ---
    public DbSet<Requisition> Requisitions => Set<Requisition>();
    public DbSet<RequisitionLine> RequisitionLines => Set<RequisitionLine>();
    public DbSet<Rfq> Rfqs => Set<Rfq>();
    public DbSet<RfqLine> RfqLines => Set<RfqLine>();
    public DbSet<VendorQuotation> VendorQuotations => Set<VendorQuotation>();
    public DbSet<VendorQuotationLine> VendorQuotationLines => Set<VendorQuotationLine>();
    public DbSet<PurchaseOrder> PurchaseOrders => Set<PurchaseOrder>();
    public DbSet<PoLine> PoLines => Set<PoLine>();

    // --- Goods Receipt (docs/10 Part C.3) ---
    public DbSet<Grn> Grns => Set<Grn>();
    public DbSet<GrnLine> GrnLines => Set<GrnLine>();
    public DbSet<GrnPayment> GrnPayments => Set<GrnPayment>();

    // --- Batch / Serial (docs/10 Part C.4) ---
    public DbSet<Batch> Batches => Set<Batch>();
    public DbSet<Serial> Serials => Set<Serial>();

    // --- Stock core: FIFO layers + immutable ledger (docs/10 Part C.5) ---
    public DbSet<StockLayer> StockLayers => Set<StockLayer>();
    public DbSet<StockLedger> StockLedger => Set<StockLedger>();

    // --- Stock transactions (docs/10 Part C.6) ---
    public DbSet<StockTransfer> StockTransfers => Set<StockTransfer>();
    public DbSet<StockTransferLine> StockTransferLines => Set<StockTransferLine>();
    public DbSet<StockAdjustment> StockAdjustments => Set<StockAdjustment>();
    public DbSet<StockAdjustmentLine> StockAdjustmentLines => Set<StockAdjustmentLine>();
    public DbSet<StockCount> StockCounts => Set<StockCount>();
    public DbSet<StockCountLine> StockCountLines => Set<StockCountLine>();

    // --- Purchase returns (docs/10 Part C.2) ---
    public DbSet<PurchaseReturn> PurchaseReturns => Set<PurchaseReturn>();
    public DbSet<PurchaseReturnLine> PurchaseReturnLines => Set<PurchaseReturnLine>();

    // --- Sales (Phase 1) ---
    public DbSet<SalesInvoice> SalesInvoices => Set<SalesInvoice>();
    public DbSet<SalesInvoiceLine> SalesInvoiceLines => Set<SalesInvoiceLine>();
    public DbSet<SalesInvoicePayment> SalesInvoicePayments => Set<SalesInvoicePayment>();
    public DbSet<SalesSlip> SalesSlips => Set<SalesSlip>();
    public DbSet<SalesSlipLine> SalesSlipLines => Set<SalesSlipLine>();
    public DbSet<SalesDayEnd> SalesDayEnds => Set<SalesDayEnd>();
    public DbSet<BundleSaleTemplate> BundleSaleTemplates => Set<BundleSaleTemplate>();
    public DbSet<BundleSaleTemplateLine> BundleSaleTemplateLines => Set<BundleSaleTemplateLine>();
    public DbSet<BundleSale> BundleSales => Set<BundleSale>();
    public DbSet<BundleSaleLine> BundleSaleLines => Set<BundleSaleLine>();
    public DbSet<SalesReturn> SalesReturns => Set<SalesReturn>();
    public DbSet<SalesReturnLine> SalesReturnLines => Set<SalesReturnLine>();

    // --- Reference data (docs/10 Part C.7) ---
    public DbSet<ReasonCode> ReasonCodes => Set<ReasonCode>();

    // --- Cross-cutting: audit trail + GL-ready journal (docs/10 Part C.7) ---
    public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
    public DbSet<JournalEntryStub> JournalEntryStubs => Set<JournalEntryStub>();

    // --- HRM: org masters (docs/12-BACKEND-HRM.md Part C.1) ---
    public DbSet<Branch> Branches => Set<Branch>();
    public DbSet<Department> Departments => Set<Department>();
    public DbSet<Designation> Designations => Set<Designation>();
    public DbSet<EmploymentType> EmploymentTypes => Set<EmploymentType>();
    public DbSet<WorkShift> WorkShifts => Set<WorkShift>();

    // --- HRM: employee core (docs/12-BACKEND-HRM.md Part C.2) ---
    public DbSet<Employee> Employees => Set<Employee>();
    public DbSet<EmployeeBankDetail> EmployeeBankDetails => Set<EmployeeBankDetail>();

    // --- HRM: staff documents (docs/12-BACKEND-HRM.md Part C.3) ---
    public DbSet<HrDocumentType> HrDocumentTypes => Set<HrDocumentType>();
    public DbSet<EmployeeDocument> EmployeeDocuments => Set<EmployeeDocument>();

    // --- HRM: attendance (docs/12-BACKEND-HRM.md Part C.4) ---
    public DbSet<AttendanceUploadBatch> AttendanceUploadBatches => Set<AttendanceUploadBatch>();
    public DbSet<AttendanceRecord> AttendanceRecords => Set<AttendanceRecord>();

    // --- HRM: leave (docs/12-BACKEND-HRM.md Part C.5) ---
    public DbSet<LeaveType> LeaveTypes => Set<LeaveType>();
    public DbSet<LeaveRequest> LeaveRequests => Set<LeaveRequest>();
    public DbSet<LeaveBalance> LeaveBalances => Set<LeaveBalance>();

    // --- HRM: payroll (docs/12-BACKEND-HRM.md Part C.6) ---
    public DbSet<SalaryComponent> SalaryComponents => Set<SalaryComponent>();
    public DbSet<EmployeeSalaryStructure> EmployeeSalaryStructures => Set<EmployeeSalaryStructure>();
    public DbSet<EmployeeSalaryStructureLine> EmployeeSalaryStructureLines => Set<EmployeeSalaryStructureLine>();
    public DbSet<EmployeeLoan> EmployeeLoans => Set<EmployeeLoan>();
    public DbSet<LoanInstallment> LoanInstallments => Set<LoanInstallment>();
    public DbSet<PayrollStatutorySetting> PayrollStatutorySettings => Set<PayrollStatutorySetting>();
    public DbSet<TaxSlab> TaxSlabs => Set<TaxSlab>();
    public DbSet<PayrollRun> PayrollRuns => Set<PayrollRun>();
    public DbSet<PayrollLine> PayrollLines => Set<PayrollLine>();
    public DbSet<PayrollLineComponent> PayrollLineComponents => Set<PayrollLineComponent>();
    public DbSet<Payslip> Payslips => Set<Payslip>();

    // --- Manufacturing: production templates (docs/30-BACKEND-PHASE2.md Part C) ---
    public DbSet<ProductionTemplate> ProductionTemplates => Set<ProductionTemplate>();
    public DbSet<TemplateStage> TemplateStages => Set<TemplateStage>();
    public DbSet<StageEdge> StageEdges => Set<StageEdge>();
    public DbSet<StageInput> StageInputs => Set<StageInput>();
    public DbSet<StageOutput> StageOutputs => Set<StageOutput>();

    // --- Manufacturing: production runs (docs/30-BACKEND-PHASE2.md Part C) ---
    public DbSet<ProductionRun> ProductionRuns => Set<ProductionRun>();
    public DbSet<RunStage> RunStages => Set<RunStage>();
    public DbSet<RunEdge> RunEdges => Set<RunEdge>();
    public DbSet<RunStageInput> RunStageInputs => Set<RunStageInput>();
    public DbSet<RunStageOutput> RunStageOutputs => Set<RunStageOutput>();
    public DbSet<RunStageEvent> RunStageEvents => Set<RunStageEvent>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Pick up every IEntityTypeConfiguration in this assembly
        // (Infra/Persistence/Configurations/*).
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ErpDbContext).Assembly);

        // Npgsql requires DateTime values written to `timestamp with time zone` columns
        // to have Kind=Utc; dates deserialized from a JSON request body (hire date,
        // salary-structure effective date, etc.) come in as Kind=Unspecified and would
        // otherwise throw at SaveChanges time. Force Utc kind globally for every
        // DateTime/DateTime? property rather than remembering to convert at each HRM
        // service call site (docs/12-BACKEND-HRM.md — new in Phase 2; Phase 1 never hit
        // this because it only ever persisted server-generated DateTime.UtcNow values).
        var utcConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<DateTime, DateTime>(
            v => v.Kind == DateTimeKind.Utc ? v : DateTime.SpecifyKind(v, DateTimeKind.Utc),
            v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
        var nullableUtcConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<DateTime?, DateTime?>(
            v => v.HasValue ? (v.Value.Kind == DateTimeKind.Utc ? v.Value : DateTime.SpecifyKind(v.Value, DateTimeKind.Utc)) : v,
            v => v.HasValue ? DateTime.SpecifyKind(v.Value, DateTimeKind.Utc) : v);

        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType == typeof(DateTime)) property.SetValueConverter(utcConverter);
                else if (property.ClrType == typeof(DateTime?)) property.SetValueConverter(nullableUtcConverter);
            }
        }
    }

    // Audit trail (FR-X-02): capture mutations before save (accurate old→new), then
    // write the log rows once inserts have their generated keys. A second base save
    // persists the logs without re-auditing them.
    public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken ct = default)
    {
        IReadOnlyList<PendingAudit> pending = _writingAuditLogs
            ? Array.Empty<PendingAudit>()
            : AuditScribe.Capture(ChangeTracker);
        var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
        if (pending.Count > 0)
        {
            try
            {
                _writingAuditLogs = true;
                WriteAuditLogs(pending);
                await base.SaveChangesAsync(acceptAllChangesOnSuccess, ct);
            }
            finally
            {
                _writingAuditLogs = false;
            }
        }
        return result;
    }

    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        IReadOnlyList<PendingAudit> pending = _writingAuditLogs
            ? Array.Empty<PendingAudit>()
            : AuditScribe.Capture(ChangeTracker);
        var result = base.SaveChanges(acceptAllChangesOnSuccess);
        if (pending.Count > 0)
        {
            try
            {
                _writingAuditLogs = true;
                WriteAuditLogs(pending);
                base.SaveChanges(acceptAllChangesOnSuccess);
            }
            finally
            {
                _writingAuditLogs = false;
            }
        }
        return result;
    }

    private void WriteAuditLogs(IReadOnlyList<PendingAudit> pending)
    {
        var now = DateTime.UtcNow;
        var userId = _currentUser.AuditUserId;
        foreach (var p in pending)
            AuditLogs.Add(AuditScribe.ToLog(p, userId, now));
    }
}
