using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using PowderCoating.Infrastructure.Data;
using PowderCoating.Shared.Constants;
using System.Drawing;
using System.IO.Compression;
using System.Text;

namespace PowderCoating.Web.Controllers;

/// <summary>
/// SuperAdmin-only controller for exporting any tenant company's operational data.
/// Supports XLSX (single workbook, multiple sheets) and CSV (per-sheet files inside a ZIP archive).
/// All queries use <c>IgnoreQueryFilters()</c> to bypass the global soft-delete and multi-tenancy
/// filters that normally restrict non-SuperAdmin users to their own company's non-deleted records.
/// </summary>
[Authorize(Policy = AppConstants.Policies.SuperAdminOnly)]
public class DataExportController : Controller
{
    private readonly ApplicationDbContext _db;

    /// <summary>
    /// Initializes the controller and sets the EPPlus license context to NonCommercial.
    /// EPPlus 5+ requires an explicit license declaration at startup or an exception is thrown;
    /// this must be done before any <see cref="ExcelPackage"/> is constructed.
    /// </summary>
    public DataExportController(ApplicationDbContext db)
    {
        _db = db;
        ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
    }

    // ── GET: Index ───────────────────────────────────────────────────────────

    /// <summary>
    /// Displays the export selection page, listing every non-deleted tenant company so the
    /// SuperAdmin can choose which company to export and which data sets to include.
    /// Uses <c>IgnoreQueryFilters()</c> because the global filter would otherwise scope results
    /// to the SuperAdmin's own company context, which would return nothing meaningful here.
    /// </summary>
    public async Task<IActionResult> Index()
    {
        var companies = await _db.Companies
            .AsNoTracking().IgnoreQueryFilters()
            .Where(c => !c.IsDeleted)
            .OrderBy(c => c.CompanyName)
            .Select(c => new CompanyExportSummary
            {
                Id            = c.Id,
                CompanyName   = c.CompanyName,
                Plan          = c.SubscriptionPlan.ToString(),
                IsActive      = c.IsActive,
                CreatedAt     = c.CreatedAt
            })
            .ToListAsync();

        return View(companies);
    }

    // ── POST: Export ─────────────────────────────────────────────────────────

    /// <summary>
    /// Accepts the company ID, selected sheet names, and desired format (<c>xlsx</c> or <c>csv</c>)
    /// and delegates to the appropriate format builder.
    /// Sheet names are reordered into a canonical order by <see cref="OrderSheets"/> so that the
    /// resulting file has a consistent layout regardless of the order the user checked the boxes.
    /// Illegal filename characters are stripped from the company name to produce a safe file name.
    /// </summary>
    /// <param name="companyId">ID of the tenant company whose data should be exported.</param>
    /// <param name="sheets">Array of sheet/entity names selected on the form (e.g. "Customers", "Jobs").</param>
    /// <param name="format">Output format: <c>"xlsx"</c> (default) or <c>"csv"</c>.</param>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> Export(int companyId, string[] sheets, string format = "xlsx")
    {
        var company = await _db.Companies
            .AsNoTracking().IgnoreQueryFilters()
            .FirstOrDefaultAsync(c => c.Id == companyId && !c.IsDeleted);

        if (company == null) return NotFound();

        if (sheets == null || sheets.Length == 0)
        {
            TempData["Error"] = "Select at least one data type to export.";
            return RedirectToAction(nameof(Index));
        }

        var safeName = string.Concat(company.CompanyName.Split(Path.GetInvalidFileNameChars()));
        var ordered  = OrderSheets(sheets);

        if (format == "csv")
            return await ExportAsCsv(companyId, company.CompanyName, safeName, ordered);

        return await ExportAsXlsx(companyId, company.CompanyName, safeName, ordered);
    }

    /// <summary>
    /// Builds the XLSX export: one EPPlus workbook with one worksheet per selected data type,
    /// plus a leading "Export Info" metadata sheet.
    /// The workbook is assembled entirely in memory (no temp files) and returned as a file download,
    /// which avoids disk I/O and temp-file cleanup concerns on the server.
    /// </summary>
    /// <param name="companyId">Tenant company ID used to filter all sheet queries.</param>
    /// <param name="companyName">Human-readable company name written into the metadata sheet.</param>
    /// <param name="safeName">Filesystem-safe company name used in the download file name.</param>
    /// <param name="ordered">Sheet names in canonical order; only listed names are added.</param>
    private async Task<IActionResult> ExportAsXlsx(int companyId, string companyName, string safeName, string[] ordered)
    {
        using var package = new ExcelPackage();
        var headerColor = Color.FromArgb(31, 78, 121);

        foreach (var sheet in ordered)
        {
            switch (sheet)
            {
                case "Customers":         await AddCustomersSheet(package, companyId, headerColor); break;
                case "CustomerContacts":  await AddCustomerContactsSheet(package, companyId, headerColor); break;
                case "Jobs":              await AddJobsSheet(package, companyId, headerColor); break;
                case "Quotes":            await AddQuotesSheet(package, companyId, headerColor); break;
                case "Invoices":          await AddInvoicesSheet(package, companyId, headerColor); break;
                case "Inventory":   await AddInventorySheet(package, companyId, headerColor); break;
                case "Equipment":   await AddEquipmentSheet(package, companyId, headerColor); break;
                case "Vendors":     await AddVendorsSheet(package, companyId, headerColor); break;
                case "Users":       await AddUsersSheet(package, companyId, headerColor); break;
            }
        }

        AddMetadataSheet(package, companyName, ordered);

        var fileName = $"{safeName}_Export_{DateTime.UtcNow:yyyyMMdd}.xlsx";
        return File(package.GetAsByteArray(),
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                    fileName);
    }

    /// <summary>
    /// Builds the CSV export: one CSV file per selected data type, bundled into a single in-memory
    /// ZIP archive that is returned as a file download.
    /// A ZIP is used rather than multiple individual file downloads because browsers do not support
    /// multi-file responses; the archive also keeps all related CSVs together for the recipient.
    /// <c>leaveOpen: true</c> is passed to <see cref="ZipArchive"/> so the underlying
    /// <see cref="MemoryStream"/> remains valid after the archive is disposed, allowing
    /// <c>ms.ToArray()</c> to capture the final bytes before the stream goes out of scope.
    /// </summary>
    /// <param name="companyId">Tenant company ID used to filter all CSV queries.</param>
    /// <param name="companyName">Human-readable company name written into the metadata CSV.</param>
    /// <param name="safeName">Filesystem-safe company name used in the download file name.</param>
    /// <param name="ordered">Sheet names in canonical order; only listed names are included.</param>
    private async Task<IActionResult> ExportAsCsv(int companyId, string companyName, string safeName, string[] ordered)
    {
        using var ms  = new MemoryStream();
        using var zip = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true);

        // Metadata entry
        var meta = new StringBuilder();
        meta.AppendLine("Field,Value");
        meta.AppendLine($"Company,{CsvEscape(companyName)}");
        meta.AppendLine($"Exported At,{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
        meta.AppendLine($"Sheets,{CsvEscape(string.Join("; ", ordered))}");
        WriteCsvEntry(zip, "Export_Info.csv", meta.ToString());

        foreach (var sheet in ordered)
        {
            switch (sheet)
            {
                case "Customers":        WriteCsvEntry(zip, "Customers.csv",        await BuildCustomersCsv(companyId)); break;
                case "CustomerContacts": WriteCsvEntry(zip, "CustomerContacts.csv", await BuildCustomerContactsCsv(companyId)); break;
                case "Jobs":             WriteCsvEntry(zip, "Jobs.csv",             await BuildJobsCsv(companyId)); break;
                case "Quotes":           WriteCsvEntry(zip, "Quotes.csv",           await BuildQuotesCsv(companyId)); break;
                case "Invoices":         WriteCsvEntry(zip, "Invoices.csv",         await BuildInvoicesCsv(companyId)); break;
                case "Inventory":   WriteCsvEntry(zip, "Inventory.csv",   await BuildInventoryCsv(companyId)); break;
                case "Equipment":   WriteCsvEntry(zip, "Equipment.csv",   await BuildEquipmentCsv(companyId)); break;
                case "Vendors":     WriteCsvEntry(zip, "Vendors.csv",     await BuildVendorsCsv(companyId)); break;
                case "Users":       WriteCsvEntry(zip, "Users.csv",       await BuildUsersCsv(companyId)); break;
            }
        }

        zip.Dispose();
        ms.Position = 0;
        var fileName = $"{safeName}_Export_{DateTime.UtcNow:yyyyMMdd}.zip";
        return File(ms.ToArray(), "application/zip", fileName);
    }

    /// <summary>
    /// Writes a single CSV string as a named entry inside an existing <see cref="ZipArchive"/>.
    /// The BOM (<c>encoderShouldEmitUTF8Identifier: true</c>) is included so that Excel opens
    /// the CSV with correct encoding without requiring an import wizard.
    /// </summary>
    /// <param name="zip">The open archive to write the entry into.</param>
    /// <param name="entryName">File name for the entry inside the ZIP (e.g. "Customers.csv").</param>
    /// <param name="content">The complete CSV text to write.</param>
    private static void WriteCsvEntry(ZipArchive zip, string entryName, string content)
    {
        var entry = zip.CreateEntry(entryName, System.IO.Compression.CompressionLevel.Optimal);
        using var writer = new StreamWriter(entry.Open(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
        writer.Write(content);
    }

    // ── Sheet builders ───────────────────────────────────────────────────────

    /// <summary>
    /// Adds an "Export Info" worksheet at position 1 of the workbook containing company name,
    /// export timestamp, and list of included sheets.
    /// Inserting this sheet first (via <c>MoveToStart</c>) gives recipients orientation context
    /// before they open the data sheets.
    /// </summary>
    /// <param name="pkg">The EPPlus workbook to add the sheet to.</param>
    /// <param name="companyName">Company name to record in the sheet.</param>
    /// <param name="sheets">Names of the data sheets included in this export.</param>
    private void AddMetadataSheet(ExcelPackage pkg, string companyName, string[] sheets)
    {
        // Insert at position 1
        var ws = pkg.Workbook.Worksheets.Add("Export Info");
        pkg.Workbook.Worksheets.MoveToStart("Export Info");

        ws.Cells[1, 1].Value = "Company";
        ws.Cells[1, 2].Value = companyName;
        ws.Cells[2, 1].Value = "Exported At";
        ws.Cells[2, 2].Value = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss UTC");
        ws.Cells[3, 1].Value = "Sheets";
        ws.Cells[3, 2].Value = string.Join(", ", sheets);

        ws.Column(1).Width = 20;
        ws.Column(2).Width = 40;
        ws.Cells[1, 1, 3, 1].Style.Font.Bold = true;
    }

    /// <summary>
    /// Adds a "Customers" worksheet with one row per non-deleted customer for the specified company.
    /// <c>IgnoreQueryFilters()</c> is required because the global EF filter would otherwise
    /// restrict results to the SuperAdmin's own tenant context.
    /// </summary>
    private async Task AddCustomersSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Customers.AsNoTracking().IgnoreQueryFilters()
            .Where(c => c.CompanyId == companyId && !c.IsDeleted)
            .OrderBy(c => c.CompanyName)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Customers");
        var headers = new[] { "ID", "Company Name", "First Name", "Last Name", "Email", "Phone",
                               "Commercial", "City", "State", "Active", "Credit Limit",
                               "Current Balance", "Lead Source",
                               "Ship-To Address", "Ship-To City", "Ship-To State", "Ship-To Zip", "Ship-To Country",
                               "Created At" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var c = data[i];
            ws.Cells[r, 1].Value  = c.Id;
            ws.Cells[r, 2].Value  = c.CompanyName;
            ws.Cells[r, 3].Value  = c.ContactFirstName;
            ws.Cells[r, 4].Value  = c.ContactLastName;
            ws.Cells[r, 5].Value  = c.Email;
            ws.Cells[r, 6].Value  = c.Phone;
            ws.Cells[r, 7].Value  = c.IsCommercial ? "Yes" : "No";
            ws.Cells[r, 8].Value  = c.City;
            ws.Cells[r, 9].Value  = c.State;
            ws.Cells[r, 10].Value = c.IsActive ? "Yes" : "No";
            ws.Cells[r, 11].Value = c.CreditLimit;
            ws.Cells[r, 12].Value = c.CurrentBalance;
            ws.Cells[r, 13].Value = c.LeadSource;
            ws.Cells[r, 14].Value = c.ShipToAddress;
            ws.Cells[r, 15].Value = c.ShipToCity;
            ws.Cells[r, 16].Value = c.ShipToState;
            ws.Cells[r, 17].Value = c.ShipToZipCode;
            ws.Cells[r, 18].Value = c.ShipToCountry;
            ws.Cells[r, 19].Value = c.CreatedAt.ToString("yyyy-MM-dd");
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds a "Jobs" worksheet with one row per non-deleted job for the specified company.
    /// Job status and priority are lookup-table entities (not enums), so they must be eagerly
    /// loaded via <c>Include</c>; the <c>DisplayName</c> navigation property is used for the
    /// human-readable label, falling back to the raw FK integer if the navigation is null.
    /// </summary>
    private async Task AddJobsSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Jobs.AsNoTracking().IgnoreQueryFilters()
            .Where(j => j.CompanyId == companyId && !j.IsDeleted)
            .Include(j => j.Customer)
            .Include(j => j.JobStatus)
            .Include(j => j.JobPriority)
            .OrderByDescending(j => j.CreatedAt)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Jobs");
        var headers = new[] { "ID", "Job Number", "Customer", "Status", "Priority",
                               "Description", "Project Name", "Due Date", "Final Price", "Created At" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var j = data[i];
            ws.Cells[r, 1].Value  = j.Id;
            ws.Cells[r, 2].Value  = j.JobNumber;
            ws.Cells[r, 3].Value  = j.Customer?.CompanyName ?? $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
            ws.Cells[r, 4].Value  = j.JobStatus?.DisplayName ?? j.JobStatusId.ToString();
            ws.Cells[r, 5].Value  = j.JobPriority?.DisplayName ?? j.JobPriorityId.ToString();
            ws.Cells[r, 6].Value  = j.Description;
            ws.Cells[r, 7].Value  = j.ProjectName;
            ws.Cells[r, 8].Value  = j.DueDate?.ToString("yyyy-MM-dd");
            ws.Cells[r, 9].Value  = j.FinalPrice;
            ws.Cells[r, 10].Value = j.CreatedAt.ToString("yyyy-MM-dd");
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds a "Quotes" worksheet with one row per non-deleted quote for the specified company.
    /// Quotes can belong to prospects (no <c>CustomerId</c>) or linked customers; the customer/prospect
    /// column shows <c>ProspectCompanyName</c> when set, otherwise falls back to the customer FK.
    /// Quote status is a lookup-table entity and is eagerly loaded so <c>DisplayName</c> is available.
    /// </summary>
    private async Task AddQuotesSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Quotes.AsNoTracking().IgnoreQueryFilters()
            .Where(q => q.CompanyId == companyId && !q.IsDeleted)
            .Include(q => q.QuoteStatus)
            .OrderByDescending(q => q.QuoteDate)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Quotes");
        var headers = new[] { "ID", "Quote Number", "Customer / Prospect", "Status",
                               "Quote Date", "Expiration Date", "Project Name", "Subtotal", "Tax", "Total" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var q = data[i];
            ws.Cells[r, 1].Value  = q.Id;
            ws.Cells[r, 2].Value  = q.QuoteNumber;
            ws.Cells[r, 3].Value  = string.IsNullOrEmpty(q.ProspectCompanyName) ? $"Customer #{q.CustomerId}" : q.ProspectCompanyName;
            ws.Cells[r, 4].Value  = q.QuoteStatus?.DisplayName ?? q.QuoteStatusId.ToString();
            ws.Cells[r, 5].Value  = q.QuoteDate.ToString("yyyy-MM-dd");
            ws.Cells[r, 6].Value  = q.ExpirationDate?.ToString("yyyy-MM-dd");
            ws.Cells[r, 7].Value  = q.ProjectName;
            ws.Cells[r, 8].Value  = q.SubTotal;
            ws.Cells[r, 9].Value  = q.TaxAmount;
            ws.Cells[r, 10].Value = q.Total;
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds an "Inventory" worksheet with one row per non-deleted inventory item for the
    /// specified company, ordered alphabetically by name for easy scanning.
    /// </summary>
    private async Task AddInventorySheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.InventoryItems.AsNoTracking().IgnoreQueryFilters()
            .Where(i => i.CompanyId == companyId && !i.IsDeleted)
            .OrderBy(i => i.Name)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Inventory");
        var headers = new[] { "ID", "Name", "SKU", "Category", "Qty on Hand",
                               "Unit", "Unit Cost", "Reorder Point", "Manufacturer", "Color" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var inv = data[i];
            ws.Cells[r, 1].Value  = inv.Id;
            ws.Cells[r, 2].Value  = inv.Name;
            ws.Cells[r, 3].Value  = inv.SKU;
            ws.Cells[r, 4].Value  = inv.Category;
            ws.Cells[r, 5].Value  = inv.QuantityOnHand;
            ws.Cells[r, 6].Value  = inv.UnitOfMeasure;
            ws.Cells[r, 7].Value  = inv.UnitCost;
            ws.Cells[r, 8].Value  = inv.ReorderPoint;
            ws.Cells[r, 9].Value  = inv.Manufacturer;
            ws.Cells[r, 10].Value = inv.ColorName;
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds an "Equipment" worksheet with one row per non-deleted equipment record for the
    /// specified company.  Equipment status is stored as an enum on the entity and is
    /// serialized via <c>ToString()</c> for a human-readable string in the export.
    /// </summary>
    private async Task AddEquipmentSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Equipment.AsNoTracking().IgnoreQueryFilters()
            .Where(e => e.CompanyId == companyId && !e.IsDeleted)
            .OrderBy(e => e.EquipmentName)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Equipment");
        var headers = new[] { "ID", "Name", "Type", "Serial Number", "Model",
                               "Status", "Purchase Date", "Purchase Price", "Next Maintenance" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var e = data[i];
            ws.Cells[r, 1].Value = e.Id;
            ws.Cells[r, 2].Value = e.EquipmentName;
            ws.Cells[r, 3].Value = e.EquipmentType;
            ws.Cells[r, 4].Value = e.SerialNumber;
            ws.Cells[r, 5].Value = e.Model;
            ws.Cells[r, 6].Value = e.Status.ToString();
            ws.Cells[r, 7].Value = e.PurchaseDate?.ToString("yyyy-MM-dd");
            ws.Cells[r, 8].Value = e.PurchasePrice;
            ws.Cells[r, 9].Value = e.NextScheduledMaintenance?.ToString("yyyy-MM-dd");
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds a "Vendors" worksheet with one row per non-deleted vendor (supplier) for the
    /// specified company, ordered alphabetically by company name.
    /// </summary>
    private async Task AddVendorsSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Vendors.AsNoTracking().IgnoreQueryFilters()
            .Where(s => s.CompanyId == companyId && !s.IsDeleted)
            .OrderBy(s => s.CompanyName)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Vendors");
        var headers = new[] { "ID", "Company Name", "Contact", "Email", "Phone",
                               "City", "State", "Preferred", "Active" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var s = data[i];
            ws.Cells[r, 1].Value = s.Id;
            ws.Cells[r, 2].Value = s.CompanyName;
            ws.Cells[r, 3].Value = s.ContactName;
            ws.Cells[r, 4].Value = s.Email;
            ws.Cells[r, 5].Value = s.Phone;
            ws.Cells[r, 6].Value = s.City;
            ws.Cells[r, 7].Value = s.State;
            ws.Cells[r, 8].Value = s.IsPreferred ? "Yes" : "No";
            ws.Cells[r, 9].Value = s.IsActive ? "Yes" : "No";
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds an "Invoices" worksheet with one row per non-deleted invoice for the specified company.
    /// The customer navigation is eagerly loaded so the customer name can be rendered; when a
    /// customer record is missing (data anomaly), the FK integer is shown as a fallback.
    /// <c>BalanceDue</c> is a computed property (<c>Total - AmountPaid</c>) that reflects partial
    /// payment status without requiring an additional query.
    /// </summary>
    private async Task AddInvoicesSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Invoices.AsNoTracking().IgnoreQueryFilters()
            .Where(i => i.CompanyId == companyId && !i.IsDeleted)
            .Include(i => i.Customer)
            .OrderByDescending(i => i.InvoiceDate)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Invoices");
        var headers = new[] { "ID", "Invoice #", "Customer", "Status", "Invoice Date",
                               "Due Date", "Project Name", "Subtotal", "Tax", "Total", "Amount Paid", "Balance Due" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var inv = data[i];
            var customerName = inv.Customer != null
                ? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
                : $"Customer #{inv.CustomerId}";
            ws.Cells[r, 1].Value  = inv.Id;
            ws.Cells[r, 2].Value  = inv.InvoiceNumber;
            ws.Cells[r, 3].Value  = customerName;
            ws.Cells[r, 4].Value  = inv.Status.ToString();
            ws.Cells[r, 5].Value  = inv.InvoiceDate.ToString("yyyy-MM-dd");
            ws.Cells[r, 6].Value  = inv.DueDate?.ToString("yyyy-MM-dd");
            ws.Cells[r, 7].Value  = inv.ProjectName;
            ws.Cells[r, 8].Value  = inv.SubTotal;
            ws.Cells[r, 9].Value  = inv.TaxAmount;
            ws.Cells[r, 10].Value = inv.Total;
            ws.Cells[r, 11].Value = inv.AmountPaid;
            ws.Cells[r, 12].Value = inv.BalanceDue;
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Adds a "Users" worksheet with one row per user belonging to the specified company.
    /// Unlike most other sheet builders this query does NOT add <c>&amp;&amp; !c.IsDeleted</c>
    /// because ASP.NET Identity users are soft-deleted by setting <c>IsActive = false</c>
    /// rather than the standard <c>IsDeleted</c> flag; all users, active or inactive, are included
    /// so the tenant has a full record for compliance/audit purposes.
    /// </summary>
    private async Task AddUsersSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.Users.AsNoTracking().IgnoreQueryFilters()
            .Where(u => u.CompanyId == companyId)
            .OrderBy(u => u.LastName)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("Users");
        var headers = new[] { "ID", "First Name", "Last Name", "Email", "Role",
                               "Active", "Hire Date", "Last Login", "Created At" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var u = data[i];
            ws.Cells[r, 1].Value = u.Id;
            ws.Cells[r, 2].Value = u.FirstName;
            ws.Cells[r, 3].Value = u.LastName;
            ws.Cells[r, 4].Value = u.Email;
            ws.Cells[r, 5].Value = u.CompanyRole;
            ws.Cells[r, 6].Value = u.IsActive ? "Yes" : "No";
            ws.Cells[r, 7].Value = u.HireDate.ToString("yyyy-MM-dd");
            ws.Cells[r, 8].Value = u.LastLoginDate?.ToString("yyyy-MM-dd") ?? "Never";
            ws.Cells[r, 9].Value = u.CreatedAt.ToString("yyyy-MM-dd");
        }
        AutoFit(ws, headers.Length);
    }

    // ── CSV builders ─────────────────────────────────────────────────────────

    /// <summary>
    /// Builds the customers CSV string for the specified company.
    /// Column names match <see cref="CustomerImportDto"/> exactly so the file can be
    /// re-imported via Tools → Bulk Import without any manual header editing.
    /// </summary>
    private async Task<string> BuildCustomersCsv(int companyId)
    {
        var data = await _db.Customers.AsNoTracking().IgnoreQueryFilters()
            .Include(c => c.PricingTier)
            .Where(c => c.CompanyId == companyId && !c.IsDeleted).OrderBy(c => c.CompanyName).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("CompanyName,ContactFirstName,ContactLastName,Email,Phone,MobilePhone,Address,City,State,ZipCode,Country,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,TaxId,IsActive,Notes,LeadSource,ShipToAddress,ShipToCity,ShipToState,ShipToZipCode,ShipToCountry");
        foreach (var c in data)
        {
            var customerType = c.IsCommercial ? "Commercial" : "Non-Commercial";
            sb.AppendLine($"{CsvEscape(c.CompanyName)},{CsvEscape(c.ContactFirstName)},{CsvEscape(c.ContactLastName)},{CsvEscape(c.Email)},{CsvEscape(c.Phone)},{CsvEscape(c.MobilePhone)},{CsvEscape(c.Address)},{CsvEscape(c.City)},{CsvEscape(c.State)},{CsvEscape(c.ZipCode)},{CsvEscape(c.Country)},{customerType},{CsvEscape(c.PricingTier?.TierName)},{c.CreditLimit},{CsvEscape(c.PaymentTerms)},{c.IsTaxExempt.ToString().ToLower()},{CsvEscape(c.TaxId)},{c.IsActive.ToString().ToLower()},{CsvEscape(c.GeneralNotes)},{CsvEscape(c.LeadSource)},{CsvEscape(c.ShipToAddress)},{CsvEscape(c.ShipToCity)},{CsvEscape(c.ShipToState)},{CsvEscape(c.ShipToZipCode)},{CsvEscape(c.ShipToCountry)}");
        }
        return sb.ToString();
    }

    /// <summary>
    /// Builds the jobs CSV string for the specified company.
    /// Column names match <see cref="JobImportDto"/> exactly so the file can be re-imported.
    /// CustomerEmail is included (not the display name) because the importer resolves
    /// the customer FK by email lookup.
    /// </summary>
    private async Task<string> BuildJobsCsv(int companyId)
    {
        var data = await _db.Jobs.AsNoTracking().IgnoreQueryFilters()
            .Where(j => j.CompanyId == companyId && !j.IsDeleted)
            .Include(j => j.Customer).Include(j => j.JobStatus).Include(j => j.JobPriority)
            .OrderByDescending(j => j.CreatedAt).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,ProjectName,FinalPrice,CustomerPO,SpecialInstructions,Notes");
        foreach (var j in data)
        {
            var customerName = !string.IsNullOrWhiteSpace(j.Customer?.CompanyName)
                ? j.Customer.CompanyName
                : $"{j.Customer?.ContactFirstName} {j.Customer?.ContactLastName}".Trim();
            sb.AppendLine($"{CsvEscape(j.JobNumber)},{CsvEscape(j.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(j.JobStatus?.DisplayName)},{CsvEscape(j.JobPriority?.DisplayName)},{j.ScheduledDate?.ToString("yyyy-MM-dd")},{j.DueDate?.ToString("yyyy-MM-dd")},{CsvEscape(j.ProjectName)},{j.FinalPrice},{CsvEscape(j.CustomerPO)},{CsvEscape(j.SpecialInstructions)},{CsvEscape(j.InternalNotes)}");
        }
        return sb.ToString();
    }

    /// <summary>
    /// Builds the quotes CSV string for the specified company.
    /// Column names match <see cref="QuoteImportDto"/> exactly so the file can be re-imported.
    /// Customer-linked quotes use CustomerEmail; prospect quotes use the ProspectCompany/Contact/Email/Phone fields.
    /// </summary>
    private async Task<string> BuildQuotesCsv(int companyId)
    {
        var data = await _db.Quotes.AsNoTracking().IgnoreQueryFilters()
            .Where(q => q.CompanyId == companyId && !q.IsDeleted)
            .Include(q => q.Customer).Include(q => q.QuoteStatus).OrderByDescending(q => q.QuoteDate).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,ProjectName,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");
        foreach (var q in data)
        {
            var customerName = !string.IsNullOrWhiteSpace(q.Customer?.CompanyName)
                ? q.Customer.CompanyName
                : $"{q.Customer?.ContactFirstName} {q.Customer?.ContactLastName}".Trim();
            sb.AppendLine($"{CsvEscape(q.QuoteNumber)},{CsvEscape(q.Customer?.Email)},{CsvEscape(customerName)},{CsvEscape(q.ProspectCompanyName)},{CsvEscape(q.ProspectContactName)},{CsvEscape(q.ProspectEmail)},{CsvEscape(q.ProspectPhone)},{CsvEscape(q.QuoteStatus?.DisplayName)},{q.QuoteDate:yyyy-MM-dd},{q.ExpirationDate?.ToString("yyyy-MM-dd")},{CsvEscape(q.ProjectName)},{q.SubTotal},{q.TaxAmount},{q.Total},{CsvEscape(q.Notes)},{CsvEscape(q.Terms)}");
        }
        return sb.ToString();
    }

    /// <summary>
    /// Builds the invoices CSV string for the specified company.
    /// Eagerly loads <c>Customer</c> so the customer name is available without a second query.
    /// When a customer record cannot be found the FK integer is used as a fallback.
    /// </summary>
    private async Task<string> BuildInvoicesCsv(int companyId)
    {
        var data = await _db.Invoices.AsNoTracking().IgnoreQueryFilters()
            .Where(i => i.CompanyId == companyId && !i.IsDeleted)
            .Include(i => i.Customer).OrderByDescending(i => i.InvoiceDate).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("ID,Invoice #,Customer,Status,Invoice Date,Due Date,Project Name,Subtotal,Tax,Total,Amount Paid,Balance Due");
        foreach (var inv in data)
        {
            var cust = inv.Customer != null
                ? (inv.Customer.CompanyName ?? $"{inv.Customer.ContactFirstName} {inv.Customer.ContactLastName}".Trim())
                : $"Customer #{inv.CustomerId}";
            sb.AppendLine($"{inv.Id},{CsvEscape(inv.InvoiceNumber)},{CsvEscape(cust)},{inv.Status},{inv.InvoiceDate:yyyy-MM-dd},{inv.DueDate?.ToString("yyyy-MM-dd")},{CsvEscape(inv.ProjectName)},{inv.SubTotal},{inv.TaxAmount},{inv.Total},{inv.AmountPaid},{inv.BalanceDue}");
        }
        return sb.ToString();
    }

    /// <summary>
    /// Adds a CustomerContacts worksheet: one row per additional contact linked to the company's customers.
    /// CustomerEmail is the join key used by the importer to re-link contacts to their parent customer.
    /// </summary>
    private async Task AddCustomerContactsSheet(ExcelPackage pkg, int companyId, Color hdr)
    {
        var data = await _db.CustomerContacts.AsNoTracking().IgnoreQueryFilters()
            .Include(cc => cc.Customer)
            .Where(cc => cc.CompanyId == companyId && !cc.IsDeleted)
            .OrderBy(cc => cc.Customer!.CompanyName).ThenBy(cc => cc.LastName).ThenBy(cc => cc.FirstName)
            .ToListAsync();

        var ws = pkg.Workbook.Worksheets.Add("CustomerContacts");
        var headers = new[] { "CustomerEmail", "FirstName", "LastName", "Title", "ContactRole", "Email", "Phone", "MobilePhone", "Notes" };
        WriteHeader(ws, headers, hdr);

        for (int i = 0; i < data.Count; i++)
        {
            var r = i + 2;
            var cc = data[i];
            ws.Cells[r, 1].Value = cc.Customer?.Email;
            ws.Cells[r, 2].Value = cc.FirstName;
            ws.Cells[r, 3].Value = cc.LastName;
            ws.Cells[r, 4].Value = cc.Title;
            ws.Cells[r, 5].Value = cc.ContactRole;
            ws.Cells[r, 6].Value = cc.Email;
            ws.Cells[r, 7].Value = cc.Phone;
            ws.Cells[r, 8].Value = cc.MobilePhone;
            ws.Cells[r, 9].Value = cc.Notes;
        }
        AutoFit(ws, headers.Length);
    }

    /// <summary>
    /// Builds the customer contacts CSV string for the specified company.
    /// CustomerEmail is the join key used by the importer to re-link contacts to their parent customer.
    /// </summary>
    private async Task<string> BuildCustomerContactsCsv(int companyId)
    {
        var data = await _db.CustomerContacts.AsNoTracking().IgnoreQueryFilters()
            .Include(cc => cc.Customer)
            .Where(cc => cc.CompanyId == companyId && !cc.IsDeleted)
            .OrderBy(cc => cc.Customer!.CompanyName).ThenBy(cc => cc.LastName).ThenBy(cc => cc.FirstName)
            .ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("CustomerEmail,FirstName,LastName,Title,ContactRole,Email,Phone,MobilePhone,Notes");
        foreach (var cc in data)
            sb.AppendLine($"{CsvEscape(cc.Customer?.Email)},{CsvEscape(cc.FirstName)},{CsvEscape(cc.LastName)},{CsvEscape(cc.Title)},{CsvEscape(cc.ContactRole)},{CsvEscape(cc.Email)},{CsvEscape(cc.Phone)},{CsvEscape(cc.MobilePhone)},{CsvEscape(cc.Notes)}");
        return sb.ToString();
    }

    /// <summary>
    /// Builds the inventory CSV string for the specified company, ordered alphabetically by name.
    /// Column names match <see cref="InventoryItemImportDto"/> exactly so the file can be re-imported.
    /// </summary>
    private async Task<string> BuildInventoryCsv(int companyId)
    {
        var data = await _db.InventoryItems.AsNoTracking().IgnoreQueryFilters()
            .Include(i => i.PrimaryVendor)
            .Where(i => i.CompanyId == companyId && !i.IsDeleted).OrderBy(i => i.Name).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("SKU,ItemName,Description,CategoryName,Manufacturer,ManufacturerPartNumber,ColorName,ColorCode,Finish,VendorName,VendorPartNumber,QuantityInStock,UnitOfMeasure,UnitCost,LastPurchasePrice,ReorderPoint,ReorderQuantity,MinimumStock,MaximumStock,CoverageSqFtPerLb,TransferEfficiencyPct,Location,IsActive,Notes");
        foreach (var i in data)
            sb.AppendLine($"{CsvEscape(i.SKU)},{CsvEscape(i.Name)},{CsvEscape(i.Description)},{CsvEscape(i.Category)},{CsvEscape(i.Manufacturer)},{CsvEscape(i.ManufacturerPartNumber)},{CsvEscape(i.ColorName)},{CsvEscape(i.ColorCode)},{CsvEscape(i.Finish)},{CsvEscape(i.PrimaryVendor?.CompanyName)},{CsvEscape(i.VendorPartNumber)},{i.QuantityOnHand},{CsvEscape(i.UnitOfMeasure)},{i.UnitCost},{i.LastPurchasePrice},{i.ReorderPoint},{i.ReorderQuantity},{i.MinimumStock},{i.MaximumStock},{i.CoverageSqFtPerLb},{i.TransferEfficiency},{i.Location},{i.IsActive.ToString().ToLower()},{CsvEscape(i.Notes)}");
        return sb.ToString();
    }

    /// <summary>
    /// Builds the equipment CSV string for the specified company, ordered alphabetically by name.
    /// Column names match <see cref="EquipmentImportDto"/> exactly so the file can be re-imported.
    /// </summary>
    private async Task<string> BuildEquipmentCsv(int companyId)
    {
        var data = await _db.Equipment.AsNoTracking().IgnoreQueryFilters()
            .Where(e => e.CompanyId == companyId && !e.IsDeleted).OrderBy(e => e.EquipmentName).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("EquipmentName,EquipmentNumber,EquipmentType,Manufacturer,Model,SerialNumber,PurchaseDate,PurchasePrice,WarrantyExpiration,Location,RecommendedMaintenanceIntervalDays,Status,IsActive,Notes");
        foreach (var e in data)
            sb.AppendLine($"{CsvEscape(e.EquipmentName)},{CsvEscape(e.EquipmentNumber)},{CsvEscape(e.EquipmentType)},{CsvEscape(e.Manufacturer)},{CsvEscape(e.Model)},{CsvEscape(e.SerialNumber)},{e.PurchaseDate?.ToString("yyyy-MM-dd")},{e.PurchasePrice},{e.WarrantyExpiration?.ToString("yyyy-MM-dd")},{CsvEscape(e.Location)},{e.RecommendedMaintenanceIntervalDays},{e.Status},{e.IsActive.ToString().ToLower()},{CsvEscape(e.Notes)}");
        return sb.ToString();
    }

    /// <summary>
    /// Builds the vendors CSV string for the specified company, ordered alphabetically by company name.
    /// Column names match <see cref="VendorImportDto"/> exactly so the file can be re-imported.
    /// </summary>
    private async Task<string> BuildVendorsCsv(int companyId)
    {
        var data = await _db.Vendors.AsNoTracking().IgnoreQueryFilters()
            .Where(s => s.CompanyId == companyId && !s.IsDeleted).OrderBy(s => s.CompanyName).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,Country,Website,AccountNumber,TaxId,PaymentTerms,CreditLimit,IsPreferred,IsActive,Notes");
        foreach (var s in data)
            sb.AppendLine($"{CsvEscape(s.CompanyName)},{CsvEscape(s.ContactName)},{CsvEscape(s.Email)},{CsvEscape(s.Phone)},{CsvEscape(s.Address)},{CsvEscape(s.City)},{CsvEscape(s.State)},{CsvEscape(s.ZipCode)},{CsvEscape(s.Country)},{CsvEscape(s.Website)},{CsvEscape(s.AccountNumber)},{CsvEscape(s.TaxId)},{CsvEscape(s.PaymentTerms)},{s.CreditLimit},{s.IsPreferred.ToString().ToLower()},{s.IsActive.ToString().ToLower()},{CsvEscape(s.Notes)}");
        return sb.ToString();
    }

    /// <summary>
    /// Builds the users CSV string for the specified company, ordered by last name.
    /// Like <see cref="AddUsersSheet"/>, the <c>IsDeleted</c> filter is intentionally omitted
    /// because Identity users use <c>IsActive</c> for soft-deletion, not the <c>IsDeleted</c> flag.
    /// </summary>
    private async Task<string> BuildUsersCsv(int companyId)
    {
        var data = await _db.Users.AsNoTracking().IgnoreQueryFilters()
            .Where(u => u.CompanyId == companyId).OrderBy(u => u.LastName).ToListAsync();
        var sb = new StringBuilder();
        sb.AppendLine("ID,First Name,Last Name,Email,Role,Active,Hire Date,Last Login,Created At");
        foreach (var u in data)
            sb.AppendLine($"{CsvEscape(u.Id)},{CsvEscape(u.FirstName)},{CsvEscape(u.LastName)},{CsvEscape(u.Email)},{CsvEscape(u.CompanyRole)},{(u.IsActive ? "Yes" : "No")},{u.HireDate:yyyy-MM-dd},{u.LastLoginDate?.ToString("yyyy-MM-dd") ?? "Never"},{u.CreatedAt:yyyy-MM-dd}");
        return sb.ToString();
    }

    // ── Utilities ────────────────────────────────────────────────────────────

    /// <summary>
    /// Writes a styled header row to the first row of an EPPlus worksheet using the specified
    /// column labels and background color.  White bold font on a dark background provides high
    /// contrast and visually separates headers from data in exported workbooks.
    /// </summary>
    /// <param name="ws">The worksheet to write headers into (always row 1).</param>
    /// <param name="headers">Ordered column header labels.</param>
    /// <param name="bgColor">Background fill color for the header row.</param>
    private static void WriteHeader(ExcelWorksheet ws, string[] headers, Color bgColor)
    {
        for (int c = 0; c < headers.Length; c++)
        {
            var cell = ws.Cells[1, c + 1];
            cell.Value = headers[c];
            cell.Style.Font.Bold = true;
            cell.Style.Font.Color.SetColor(Color.White);
            cell.Style.Fill.PatternType = ExcelFillStyle.Solid;
            cell.Style.Fill.BackgroundColor.SetColor(bgColor);
        }
    }

    /// <summary>
    /// Auto-fits all columns in the worksheet to their content width, then caps each column at
    /// 50 characters to prevent excessively wide columns caused by long free-text fields
    /// (e.g., job descriptions or notes).
    /// </summary>
    /// <param name="ws">The worksheet to auto-fit.</param>
    /// <param name="colCount">Total number of data columns, used for the width-cap loop.</param>
    private static void AutoFit(ExcelWorksheet ws, int colCount)
    {
        ws.Cells[ws.Dimension?.Address ?? "A1"].AutoFitColumns();
        // Cap column width
        for (int c = 1; c <= colCount; c++)
        {
            if (ws.Column(c).Width > 50)
                ws.Column(c).Width = 50;
        }
    }

    /// <summary>
    /// Returns the requested sheet names sorted into the canonical export order
    /// (Customers → Jobs → Quotes → Invoices → Inventory → Equipment → Vendors → Users).
    /// This ensures that the workbook and ZIP archive always have a predictable, logical layout
    /// regardless of the order the administrator checked the boxes on the form.
    /// Any sheet name not in the canonical list is silently ignored.
    /// </summary>
    /// <param name="sheets">Raw sheet names from the form POST.</param>
    private static string[] OrderSheets(string[] sheets)
    {
        var order = new[] { "Customers", "Jobs", "Quotes", "Invoices", "Inventory", "Equipment", "Vendors", "Users" };
        return order.Where(sheets.Contains).ToArray();
    }

    /// <summary>
    /// RFC 4180-compliant CSV field escaper.  Wraps the value in double-quotes and escapes
    /// embedded double-quotes (by doubling them) whenever the value contains a comma,
    /// double-quote, carriage return, or line feed.  Returns an empty string for null values
    /// so CSV rows never contain bare null literals.
    /// </summary>
    /// <param name="value">The field value to escape; may be null or any object type.</param>
    private static string CsvEscape(object? value)
    {
        if (value == null) return "";
        var s = value.ToString() ?? "";
        if (s.Contains(',') || s.Contains('"') || s.Contains('\n') || s.Contains('\r'))
            return $"\"{s.Replace("\"", "\"\"")}\"";
        return s;
    }
}

// ── DTOs ─────────────────────────────────────────────────────────────────────

/// <summary>
/// Lightweight projection used on the export index page to list available tenant companies
/// without loading every column from the Companies table.
/// </summary>
public class CompanyExportSummary
{
    public int Id { get; set; }
    public string CompanyName { get; set; } = "";
    public string Plan { get; set; } = "";
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
}
