using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using PowderCoating.Application.DTOs.Import;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Enums;
using PowderCoating.Core.Interfaces;
using PowderCoating.Infrastructure.Data;
using System.Security.Claims;

namespace PowderCoating.Web.Controllers;

[Authorize]
public class ToolsController : Controller
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IQuickBooksIifService _quickBooksService;
    private readonly ICsvImportService _csvImportService;
    private readonly ITenantContext _tenantContext;
    private readonly ILogger<ToolsController> _logger;
    private readonly UserManager<ApplicationUser> _userManager;
    private readonly IAccountBalanceService _accountBalanceService;
    private readonly PowderCoating.Application.Interfaces.IAuditService _auditService;

    public ToolsController(
        IUnitOfWork unitOfWork,
        IQuickBooksIifService quickBooksService,
        ICsvImportService csvImportService,
        ITenantContext tenantContext,
        ILogger<ToolsController> logger,
        UserManager<ApplicationUser> userManager,
        IAccountBalanceService accountBalanceService,
        PowderCoating.Application.Interfaces.IAuditService auditService)
    {
        _unitOfWork = unitOfWork;
        _quickBooksService = quickBooksService;
        _csvImportService = csvImportService;
        _tenantContext = tenantContext;
        _logger = logger;
        _userManager = userManager;
        _accountBalanceService = accountBalanceService;
        _auditService = auditService;
    }

    private Task LogImportAsync(string entityType, string fileName,
        PowderCoating.Application.DTOs.QuickBooks.ImportResultDto r) =>
        _auditService.LogAsync("Imported", entityType,
            $"{fileName}: {r.Summary}",
            new { fileName, r.TotalRecords, r.ImportedCount, r.UpdatedCount, r.SkippedCount, Errors = r.Errors.Count });

    private Task LogCsvImportAsync(string entityType, string fileName,
        PowderCoating.Application.DTOs.Import.CsvImportResultDto r) =>
        _auditService.LogAsync("Imported", entityType,
            $"{fileName}: {r.Summary}",
            new { fileName, r.TotalRows, r.SuccessCount, r.ErrorCount, r.SkippedCount });

    private Task LogExportAsync(string entityType, string description) =>
        _auditService.LogAsync("Exported", entityType, description);

    /// <summary>
    /// Renders the Tools hub page, which exposes QuickBooks IIF/CSV import/export,
    /// bulk CSV import/export, the worker randomizer wheel, and QuickBooks Online import tabs.
    /// Redirects to Home if the user is not bound to a company (i.e., a SuperAdmin with no
    /// active tenant context), because all tools are company-scoped.
    /// </summary>
    // GET: Tools
    public async Task<IActionResult> Index()
    {
        var companyId = _tenantContext.GetCurrentCompanyId();
        if (companyId == null)
        {
            TempData["ErrorMessage"] = "Your account is not associated with a company. Please contact support.";
            return RedirectToAction("Index", "Home");
        }

        await PopulateImportAccountDropdownsAsync();
        return View();
    }

    /// <summary>
    /// Returns the current GL account lists as JSON so the import wizard can refresh dropdowns
    /// without a full page reload. Called via AJAX when the catalog or inventory import card opens.
    /// </summary>
    // GET: Tools/GetImportAccounts
    [HttpGet]
    public async Task<IActionResult> GetImportAccounts()
    {
        var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var allAccounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId);

        var revenue = allAccounts
            .Where(a => a.AccountType == AccountType.Revenue && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .Select(a => new { value = a.Id.ToString(), text = $"{a.AccountNumber} – {a.Name}" })
            .Prepend(new { value = "", text = "— None —" });

        var cogs = allAccounts
            .Where(a => a.AccountType == AccountType.CostOfGoods && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .Select(a => new { value = a.Id.ToString(), text = $"{a.AccountNumber} – {a.Name}" })
            .Prepend(new { value = "", text = "— None —" });

        var inventory = allAccounts
            .Where(a => a.AccountType == AccountType.Asset && a.AccountSubType == AccountSubType.Inventory && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .Select(a => new { value = a.Id.ToString(), text = $"{a.AccountNumber} – {a.Name}" })
            .Prepend(new { value = "", text = "— None —" });

        return Json(new
        {
            revenueAccounts   = revenue,
            cogsAccounts      = cogs,
            inventoryAccounts = inventory
        });
    }

    /// <summary>
    /// Populates ViewBag dropdowns used by CSV import forms that need an optional GL account
    /// override (e.g., Revenue, COGS, Inventory Asset accounts). The "— None —" sentinel at
    /// index 0 means "use whatever the import service defaults to" so callers can pass null.
    /// Queries are company-scoped via global query filters; no ignoreQueryFilters needed here.
    /// </summary>
    private async Task PopulateImportAccountDropdownsAsync()
    {
        var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
        var allAccounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId);

        var revenueAccounts = allAccounts
            .Where(a => a.AccountType == AccountType.Revenue && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
            .ToList();
        revenueAccounts.Insert(0, new SelectListItem("— None —", ""));
        ViewBag.RevenueAccounts = revenueAccounts;

        var cogsAccounts = allAccounts
            .Where(a => a.AccountType == AccountType.CostOfGoods && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
            .ToList();
        cogsAccounts.Insert(0, new SelectListItem("— None —", ""));
        ViewBag.CogsAccounts = cogsAccounts;

        var inventoryAccounts = allAccounts
            .Where(a => a.AccountType == AccountType.Asset && a.AccountSubType == AccountSubType.Inventory && a.IsActive)
            .OrderBy(a => a.AccountNumber)
            .Select(a => new SelectListItem($"{a.AccountNumber} – {a.Name}", a.Id.ToString()))
            .ToList();
        inventoryAccounts.Insert(0, new SelectListItem("— None —", ""));
        ViewBag.InventoryAccounts = inventoryAccounts;
    }

    /// <summary>
    /// Exports all customers for the current company in QuickBooks IIF format (desktop) or
    /// CSV format (online), delegating format-specific logic to <see cref="IQuickBooksIifService"/>.
    /// The <paramref name="format"/> query-string parameter defaults to "desktop" (IIF / text/plain);
    /// pass "online" to get a CSV suitable for QuickBooks Online's import wizard.
    /// Returns the file directly on success; sets TempData["ErrorMessage"] and redirects on failure.
    /// </summary>
    // GET: Tools/ExportCustomers
    [HttpGet]
    public async Task<IActionResult> ExportCustomers(string format = "desktop")
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            _logger.LogInformation("User {UserName} exporting customers for company {CompanyId} in {Format} format",
                User.Identity?.Name, companyId, format);

            var result = format.ToLower() == "online"
                ? await _quickBooksService.ExportCustomersOnlineAsync(companyId.Value)
                : await _quickBooksService.ExportCustomersAsync(companyId.Value);

            if (!result.Success)
            {
                _logger.LogWarning("Export customers failed: {ErrorMessage}", result.ErrorMessage);
                TempData["ErrorMessage"] = result.ErrorMessage;
                return RedirectToAction(nameof(Index));
            }

            var contentType = format.ToLower() == "online" ? "text/csv" : "text/plain";
            await LogExportAsync("Customers", $"QB {format} export");
            return File(result.FileContent, contentType, result.FileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting customers");
            TempData["ErrorMessage"] = "An error occurred while exporting customers.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports the company's service catalog in QuickBooks IIF (desktop) or CSV (online) format.
    /// Mirrors <see cref="ExportCustomers"/> in structure; "online" produces a CSV that QuickBooks
    /// Online can import as Products &amp; Services, while "desktop" produces an IIF file for
    /// QuickBooks Desktop item import.
    /// </summary>
    // GET: Tools/ExportCatalogItems
    [HttpGet]
    public async Task<IActionResult> ExportCatalogItems(string format = "desktop")
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            _logger.LogInformation("User {UserName} exporting catalog items for company {CompanyId} in {Format} format",
                User.Identity?.Name, companyId, format);

            var result = format.ToLower() == "online"
                ? await _quickBooksService.ExportCatalogItemsOnlineAsync(companyId.Value)
                : await _quickBooksService.ExportCatalogItemsAsync(companyId.Value);

            if (!result.Success)
            {
                _logger.LogWarning("Export catalog items failed: {ErrorMessage}", result.ErrorMessage);
                TempData["ErrorMessage"] = result.ErrorMessage;
                return RedirectToAction(nameof(Index));
            }

            var contentType = format.ToLower() == "online" ? "text/csv" : "text/plain";
            await LogExportAsync("CatalogItems", $"QB {format} export");
            return File(result.FileContent, contentType, result.FileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting catalog items");
            TempData["ErrorMessage"] = "An error occurred while exporting catalog items.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Accepts a QuickBooks-exported IIF or CSV customer file and upserts customers for the current
    /// company via <see cref="IQuickBooksIifService"/>. Returns JSON so the page can display a
    /// per-row error/warning accordion without a full page reload. The response always includes
    /// row-level error details (severity, lineNumber, recordName, fieldName, errorMessage,
    /// displayMessage) to let the user drill into any skipped or failed records.
    /// </summary>
    // POST: Tools/ImportCustomers
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportCustomers(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new
                {
                    success = false,
                    message = "Your account is not associated with a company."
                });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new
                {
                    success = false,
                    message = "No file provided or file is empty."
                });
            }

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing customers from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportCustomersAsync(file, companyId.Value, userId);
            await LogImportAsync("Customers", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Success ? "Import completed successfully!" : "Import completed with errors.",
                totalRecords = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount = result.UpdatedCount,
                skippedCount = result.SkippedCount,
                errors = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing customers");
            return Json(new
            {
                success = false,
                message = $"An error occurred during import: {ex.Message}"
            });
        }
    }

    /// <summary>
    /// Exports the company's vendor list as a QuickBooks Desktop IIF file (text/plain).
    /// Vendor export is desktop-only; there is no "online" variant because the QB Online vendor
    /// CSV format is handled by the separate QBO import/export endpoints.
    /// </summary>
    // GET: Tools/ExportVendors
    [HttpGet]
    public async Task<IActionResult> ExportVendors()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            _logger.LogInformation("User {UserName} exporting vendors for company {CompanyId}",
                User.Identity?.Name, companyId);

            var result = await _quickBooksService.ExportVendorsAsync(companyId.Value);

            if (!result.Success)
            {
                TempData["ErrorMessage"] = result.ErrorMessage;
                return RedirectToAction(nameof(Index));
            }

            await LogExportAsync("Vendors", "QB IIF export");
            return File(result.FileContent, "text/plain", result.FileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting vendors");
            TempData["ErrorMessage"] = "An error occurred while exporting vendors.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Imports vendors from a QuickBooks-exported IIF or CSV file, upserting records for the
    /// current company. Returns JSON with the same row-level detail structure as
    /// <see cref="ImportCustomers"/> so the UI can render a consistent error accordion.
    /// </summary>
    // POST: Tools/ImportVendors
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportVendors(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing vendors from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportVendorsAsync(file, companyId.Value, userId);
            await LogImportAsync("Vendors", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Success ? "Import completed successfully!" : "Import completed with errors.",
                totalRecords = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount = result.UpdatedCount,
                skippedCount = result.SkippedCount,
                errors = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing vendors");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Imports historical invoices from a QuickBooks CSV export (e.g., Transaction List by Date).
    /// After importing, calls <see cref="IAccountBalanceService.RecalculateAllAsync"/> to bring
    /// every customer's <c>CurrentBalance</c> and every GL account balance into sync, because
    /// bulk invoice imports can shift outstanding AR totals significantly.
    /// </summary>
    // POST: Tools/ImportQbInvoices
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQbInvoices(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing QB invoices from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportQbInvoicesFromCsvAsync(file, companyId.Value, userId);
            await LogImportAsync("Invoices", file.FileName, result);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            return Json(new
            {
                success = result.Success,
                message = result.Success ? "Invoice import completed!" : "Import completed with errors.",
                totalRecords = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount = result.UpdatedCount,
                skippedCount = result.SkippedCount,
                errors = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QB invoices");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Imports general-ledger transactions from a QuickBooks CSV export. Like
    /// <see cref="ImportQbInvoices"/>, triggers a full account-balance recalculation after the
    /// import because journal-entry style transactions directly affect GL account running totals.
    /// </summary>
    // POST: Tools/ImportQbTransactions
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQbTransactions(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing QB transactions from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportQbTransactionsFromCsvAsync(file, companyId.Value, userId);
            await LogImportAsync("Transactions", file.FileName, result);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            return Json(new
            {
                success = result.Success,
                message = result.Success ? "Transaction import completed!" : "Import completed with errors.",
                totalRecords = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount = result.UpdatedCount,
                skippedCount = result.SkippedCount,
                errors = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QB transactions");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Imports service catalog items from a QuickBooks IIF or CSV file, creating or updating
    /// catalog items for the current company. No account-balance recalculation is needed because
    /// catalog items carry pricing metadata but are not themselves financial transactions.
    /// </summary>
    // POST: Tools/ImportCatalogItems
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportCatalogItems(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new
                {
                    success = false,
                    message = "Your account is not associated with a company."
                });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new
                {
                    success = false,
                    message = "No file provided or file is empty."
                });
            }

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing catalog items from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportCatalogItemsAsync(file, companyId.Value, userId);
            await LogImportAsync("CatalogItems", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Success ? "Import completed successfully!" : "Import completed with errors.",
                totalRecords = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount = result.UpdatedCount,
                skippedCount = result.SkippedCount,
                errors = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing catalog items");
            return Json(new
            {
                success = false,
                message = $"An error occurred during import: {ex.Message}"
            });
        }
    }

    /// <summary>
    /// Imports the Chart of Accounts from a QuickBooks CSV export, creating or updating GL accounts
    /// for the current company. Triggers a full balance recalculation because account metadata
    /// (type, subtype) directly affects how balances are aggregated in financial reports — a
    /// changed account type can shift an amount from Assets to Expenses, for example.
    /// </summary>
    // POST: Tools/ImportChartOfAccounts
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportChartOfAccounts(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing Chart of Accounts from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportChartOfAccountsAsync(file, companyId.Value, userId);
            await LogImportAsync("ChartOfAccounts", file.FileName, result);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            return Json(new
            {
                success       = result.Success,
                message       = result.Success ? "Chart of Accounts imported successfully!" : "Import completed with errors.",
                totalRecords  = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount  = result.UpdatedCount,
                skippedCount  = result.SkippedCount,
                errors        = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing Chart of Accounts");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Imports vendor payments (bill payment checks/EFTs) from a QuickBooks CSV export and
    /// reconciles them against existing vendor bills. Triggers account-balance recalculation
    /// because payments reduce AP balances and affect cash/bank accounts. Use
    /// <see cref="ImportQbBillsAndPayments"/> instead when the source file contains both bills
    /// and their associated payments (e.g., a Vendor Balance Detail report), which avoids the
    /// need to upload two separate files.
    /// </summary>
    // POST: Tools/ImportQbVendorPayments
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQbVendorPayments(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing QB vendor payments from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportQbVendorPaymentsAsync(file, companyId.Value, userId);
            await LogImportAsync("VendorPayments", file.FileName, result);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            return Json(new
            {
                success       = result.Success,
                message       = result.Success ? "Vendor payments imported successfully!" : "Import completed with errors.",
                totalRecords  = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount  = result.UpdatedCount,
                skippedCount  = result.SkippedCount,
                errors        = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QB vendor payments");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Combined action that processes a single QuickBooks Vendor Balance Detail CSV in two passes:
    /// first importing all bills, then applying payments to those bills via FIFO matching.
    /// The file is buffered into a <see cref="System.IO.MemoryStream"/> once so two independent
    /// <see cref="IFormFile"/> wrappers can be created — one per service call — without re-reading
    /// the original upload stream (which would be exhausted after the first pass). "Bill skipped"
    /// messages from the payments pass are suppressed in the combined error list because every
    /// bill-type row in the file will naturally be skipped by the payment importer; surfacing those
    /// would create noise that masks real payment-matching failures.
    /// </summary>
    // POST: Tools/ImportQbBillsAndPayments
    // Combined action: runs Bills pass then Vendor Payments pass on the same file (Vendor Balance Detail).
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQbBillsAndPayments(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing QB bills+payments from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            // Read the file bytes once so we can pass two independent streams to the service
            using var ms = new System.IO.MemoryStream();
            await file.OpenReadStream().CopyToAsync(ms);
            var fileBytes = ms.ToArray();

            IFormFile MakeFormFile() => new FormFile(
                new System.IO.MemoryStream(fileBytes), 0, fileBytes.Length,
                file.Name, file.FileName)
            {
                Headers = file.Headers,
                ContentType = file.ContentType
            };

            // Pass 1: Bills
            var billsResult = await _quickBooksService.ImportQbBillsAsync(MakeFormFile(), companyId.Value, userId);

            // Pass 2: Vendor Payments (FIFO against just-imported bills)
            var pmtResult = await _quickBooksService.ImportQbVendorPaymentsAsync(MakeFormFile(), companyId.Value, userId);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            await LogImportAsync("Bills", file.FileName, billsResult);
            await LogImportAsync("VendorPayments", file.FileName, pmtResult);

            // Combine results — only surface non-"Bill" skipped messages from payments pass
            // (the payments pass will generate "Bill skipped" messages for every Bill row, suppress those)
            var filteredPmtErrors = pmtResult.Errors
                .Where(e => !(e.Severity == "Skipped" && e.ErrorMessage != null &&
                              e.ErrorMessage.Contains("not a bill payment", StringComparison.OrdinalIgnoreCase)))
                .ToList();

            var allErrors = billsResult.Errors.Concat(filteredPmtErrors).ToList();
            bool overallSuccess = billsResult.Success && pmtResult.Success;

            return Json(new
            {
                success       = overallSuccess,
                message       = overallSuccess
                    ? $"Bills & payments imported successfully!"
                    : "Import completed with some errors — see details below.",
                billsImported    = billsResult.ImportedCount,
                paymentsImported = pmtResult.ImportedCount,
                totalRecords         = billsResult.TotalRecords + pmtResult.TotalRecords,
                importedCount        = billsResult.ImportedCount + pmtResult.ImportedCount,
                updatedCount         = billsResult.UpdatedCount + pmtResult.UpdatedCount,
                skippedCount         = billsResult.SkippedCount + pmtResult.SkippedCount,
                alreadyRecordedCount = billsResult.AlreadyRecordedCount + pmtResult.AlreadyRecordedCount,
                errors = allErrors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QB bills and payments");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Imports vendor bills (AP invoices) from a QuickBooks CSV export. Use this when you have a
    /// bills-only export file; use <see cref="ImportQbBillsAndPayments"/> when the same file also
    /// contains payment rows. Triggers a full account-balance recalculation because each new bill
    /// increases AP and may affect expense account balances.
    /// </summary>
    // POST: Tools/ImportQbBills
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQbBills(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing QB bills from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportQbBillsAsync(file, companyId.Value, userId);
            await LogImportAsync("Bills", file.FileName, result);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            return Json(new
            {
                success       = result.Success,
                message       = result.Success ? "Bills imported successfully!" : "Import completed with errors.",
                totalRecords  = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount  = result.UpdatedCount,
                skippedCount  = result.SkippedCount,
                errors        = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QB bills");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Imports inventory quantities and valuations from a QuickBooks Inventory Valuation Summary CSV.
    /// Triggers a balance recalculation because imported on-hand quantities feed into the Inventory
    /// Asset account balance used on the Balance Sheet report.
    /// </summary>
    // POST: Tools/ImportQbInventoryValuation
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQbInventoryValuation(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";

            _logger.LogInformation("User {UserName} importing QB inventory valuation from {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            var result = await _quickBooksService.ImportQbInventoryValuationAsync(file, companyId.Value, userId);
            await LogImportAsync("InventoryValuation", file.FileName, result);

            await _accountBalanceService.RecalculateAllAsync(companyId.Value);

            return Json(new
            {
                success       = result.Success,
                message       = result.Success ? "Inventory import completed successfully!" : "Import completed with errors.",
                totalRecords  = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount  = result.UpdatedCount,
                skippedCount  = result.SkippedCount,
                errors        = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QB inventory valuation");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Returns the full names of all active users in the current company as a JSON array, used
    /// by the shop-floor randomizer (spinning wheel) feature on the Tools page. Queries
    /// <see cref="UserManager{TUser}"/> directly rather than going through <see cref="IUnitOfWork"/>
    /// because Identity users are not exposed as a first-class repository; the filter by
    /// <c>CompanyId</c> provides the multi-tenant isolation that global query filters would
    /// normally enforce for other entity types.
    /// </summary>
    // GET: Tools/GetShopWorkers - Returns active company users for randomizer wheel
    [HttpGet]
    public async Task<IActionResult> GetShopWorkers()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId() ?? 0;
            var users = await _userManager.Users
                .Where(u => u.CompanyId == companyId && u.IsActive)
                .OrderBy(u => u.FirstName).ThenBy(u => u.LastName)
                .ToListAsync();
            var workerNames = users
                .Select(u => u.FullName)
                .ToList();

            return Json(new
            {
                success = true,
                workers = workerNames
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error fetching shop workers for randomizer");
            return Json(new
            {
                success = false,
                message = "Error loading shop workers"
            });
        }
    }

    #region CSV Bulk Import

    /// <summary>
    /// Downloads a blank CSV file pre-populated with the correct column headers for the native
    /// (non-QuickBooks) customer bulk import. Timestamps the filename to prevent browser caching
    /// when users download a fresh copy after a column layout change.
    /// </summary>
    // GET: Tools/DownloadCustomerTemplate
    [HttpGet]
    public IActionResult DownloadCustomerTemplate()
    {
        try
        {
            var csvContent = _csvImportService.GenerateCustomerTemplate();
            var fileName = $"customer_import_template_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
            return File(csvContent, "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating customer template");
            TempData["ErrorMessage"] = "An error occurred while generating the template.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native catalog-item bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportCatalogItems"/>.
    /// </summary>
    // GET: Tools/DownloadCatalogTemplate
    [HttpGet]
    public IActionResult DownloadCatalogTemplate()
    {
        try
        {
            var csvContent = _csvImportService.GenerateCatalogItemTemplate();
            var fileName = $"catalog_import_template_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
            return File(csvContent, "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating catalog template");
            TempData["ErrorMessage"] = "An error occurred while generating the template.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native inventory-item bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportInventoryItems"/>.
    /// </summary>
    // GET: Tools/DownloadInventoryTemplate
    [HttpGet]
    public IActionResult DownloadInventoryTemplate()
    {
        try
        {
            var csvContent = _csvImportService.GenerateInventoryItemTemplate();
            var fileName = $"inventory_import_template_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
            return File(csvContent, "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating inventory template");
            TempData["ErrorMessage"] = "An error occurred while generating the template.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Accepts a native CSV file (not QuickBooks-formatted) and bulk-imports customers for the
    /// current company via <see cref="ICsvImportService"/>. The service owns all upsert logic;
    /// this action only validates the file type (must be .csv), resolves the company, and
    /// returns a JSON result with per-row success/error/warning detail for the UI accordion.
    /// </summary>
    // POST: Tools/CsvImportCustomers
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportCustomers(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing customers from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportCustomersAsync(stream, companyId.Value);
            await LogCsvImportAsync("Customers", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing customers from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Bulk-imports service catalog items from a native CSV file. The optional
    /// <paramref name="revenueAccountId"/> and <paramref name="cogsAccountId"/> parameters let
    /// the user map all imported items to specific GL accounts in one pass, avoiding the need to
    /// edit each item individually afterward. Passing null for either account defers to the
    /// service's default mapping logic.
    /// </summary>
    // POST: Tools/CsvImportCatalogItems
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportCatalogItems(IFormFile file, int? revenueAccountId, int? cogsAccountId)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing catalog items from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            // Validate account IDs belong to this company — stale page load can produce IDs
            // that were valid before a data reset but no longer exist.
            var validAccountIds = (await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value))
                .Select(a => a.Id).ToHashSet();
            if (revenueAccountId.HasValue && !validAccountIds.Contains(revenueAccountId.Value))
                revenueAccountId = null;
            if (cogsAccountId.HasValue && !validAccountIds.Contains(cogsAccountId.Value))
                cogsAccountId = null;

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportCatalogItemsAsync(stream, companyId.Value, revenueAccountId, cogsAccountId);
            await LogCsvImportAsync("CatalogItems", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing catalog items from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Bulk-imports inventory items (powder coatings, consumables, etc.) from a native CSV file.
    /// Like <see cref="CsvImportCatalogItems"/>, the optional GL account overrides
    /// (<paramref name="inventoryAccountId"/> for the balance-sheet Inventory Asset account and
    /// <paramref name="cogsAccountId"/> for the income-statement COGS account) let the user
    /// pre-assign accounts during the import rather than editing each item afterward.
    /// </summary>
    // POST: Tools/CsvImportInventoryItems
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportInventoryItems(IFormFile file, int? inventoryAccountId, int? cogsAccountId)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing inventory items from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            // Validate account IDs belong to this company — stale page load can produce IDs
            // that were valid before a data reset but no longer exist.
            var validAccountIds = (await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value))
                .Select(a => a.Id).ToHashSet();
            if (inventoryAccountId.HasValue && !validAccountIds.Contains(inventoryAccountId.Value))
                inventoryAccountId = null;
            if (cogsAccountId.HasValue && !validAccountIds.Contains(cogsAccountId.Value))
                cogsAccountId = null;

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportInventoryItemsAsync(stream, companyId.Value, inventoryAccountId, cogsAccountId);
            await LogCsvImportAsync("InventoryItems", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing inventory items from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native quote bulk import, with headers matching
    /// the columns expected by <see cref="CsvImportQuotes"/>.
    /// </summary>
    // GET: Tools/DownloadQuoteTemplate
    [HttpGet]
    public IActionResult DownloadQuoteTemplate()
    {
        var csvBytes = _csvImportService.GenerateQuoteTemplate();
        return File(csvBytes, "text/csv", "quote_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports quotes from a native CSV file. Quotes can reference existing customers by
    /// name or ID; the import service handles customer resolution. Returns JSON with per-row
    /// status so the UI can display a success/error breakdown without a page reload.
    /// </summary>
    // POST: Tools/CsvImportQuotes
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportQuotes(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing quotes from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportQuotesAsync(stream, companyId.Value);
            await LogCsvImportAsync("Quotes", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing quotes from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native job bulk import, with headers matching
    /// the columns expected by <see cref="CsvImportJobs"/>.
    /// </summary>
    // GET: Tools/DownloadJobTemplate
    [HttpGet]
    public IActionResult DownloadJobTemplate()
    {
        var csvBytes = _csvImportService.GenerateJobTemplate();
        return File(csvBytes, "text/csv", "job_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports jobs from a native CSV file. Job status and priority are lookup-table
    /// entities (not enums), so the import service must resolve <c>StatusCode</c> and
    /// <c>PriorityCode</c> strings against the company's current lookup tables. Returns JSON
    /// with per-row status detail for the UI accordion.
    /// </summary>
    // POST: Tools/CsvImportJobs
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportJobs(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing jobs from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportJobsAsync(stream, companyId.Value);
            await LogCsvImportAsync("Jobs", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing jobs from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native invoice bulk import, with headers matching
    /// the columns expected by <see cref="CsvImportInvoices"/> and produced by
    /// <see cref="ExportInvoicesCsv"/> for round-trip compatibility.
    /// </summary>
    // GET: Tools/DownloadInvoiceTemplate
    [HttpGet]
    public IActionResult DownloadInvoiceTemplate()
    {
        var csvBytes = _csvImportService.GenerateInvoiceTemplate();
        return File(csvBytes, "text/csv", "invoice_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports invoice headers from a native CSV file. Customers are resolved by
    /// CustomerEmail then CustomerName; rows without a match are skipped. Existing invoices
    /// matched by InvoiceNumber are updated; new ones are created. Returns JSON with per-row
    /// status so the UI can display a success/error breakdown without a page reload.
    /// </summary>
    // POST: Tools/CsvImportInvoices
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportInvoices(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing invoices from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportInvoicesAsync(stream, companyId.Value);
            await LogCsvImportAsync("Invoices", file.FileName, result);

            return Json(new
            {
                success      = result.Success,
                message      = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount   = result.ErrorCount,
                totalRows    = result.TotalRows,
                errors       = result.Errors,
                warnings     = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing invoices from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Bulk-imports invoice line items from a native CSV file. Lines are matched to their parent
    /// invoice by InvoiceNumber and revenue accounts resolved by number. Run after the invoice import.
    /// </summary>
    // POST: Tools/CsvImportInvoiceItems
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportInvoiceItems(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing invoice items from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportInvoiceItemsAsync(stream, companyId.Value);
            await LogCsvImportAsync("InvoiceItems", file.FileName, result);

            return Json(new
            {
                success      = result.Success,
                message      = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount   = result.ErrorCount,
                totalRows    = result.TotalRows,
                errors       = result.Errors,
                warnings     = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing invoice items from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native payment bulk import.
    /// Columns match the native ExportPaymentsCsv output for round-trip compatibility.
    /// </summary>
    // GET: Tools/DownloadPaymentTemplate
    [HttpGet]
    public IActionResult DownloadPaymentTemplate()
    {
        var csvBytes = _csvImportService.GeneratePaymentTemplate();
        return File(csvBytes, "text/csv", "payment_import_template.csv");
    }

    /// <summary>Downloads a blank CSV template for the invoice line-item bulk import.</summary>
    // GET: Tools/DownloadInvoiceItemTemplate
    [HttpGet]
    public IActionResult DownloadInvoiceItemTemplate()
    {
        var csvBytes = _csvImportService.GenerateInvoiceItemTemplate();
        return File(csvBytes, "text/csv", "invoice_item_import_template.csv");
    }

    // POST: Tools/CsvImportBills — vendor bill headers (vendor by name, AP account by number).
    [HttpPost]
    [ValidateAntiForgeryToken]
    public Task<IActionResult> CsvImportBills(IFormFile file)
        => RunCsvImport(file, "Bills", _csvImportService.ImportBillsAsync);

    // POST: Tools/CsvImportBillLineItems — bill lines (run after bills).
    [HttpPost]
    [ValidateAntiForgeryToken]
    public Task<IActionResult> CsvImportBillLineItems(IFormFile file)
        => RunCsvImport(file, "BillLineItems", _csvImportService.ImportBillLineItemsAsync);

    // POST: Tools/CsvImportDeposits — customer deposits (customer by name, bank account by number).
    [HttpPost]
    [ValidateAntiForgeryToken]
    public Task<IActionResult> CsvImportDeposits(IFormFile file)
        => RunCsvImport(file, "Deposits", _csvImportService.ImportDepositsAsync);

    // POST: Tools/CsvImportJournalEntries — journal entry headers.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public Task<IActionResult> CsvImportJournalEntries(IFormFile file)
        => RunCsvImport(file, "JournalEntries", _csvImportService.ImportJournalEntriesAsync);

    // POST: Tools/CsvImportJournalEntryLines — journal entry lines (run after journal entries).
    [HttpPost]
    [ValidateAntiForgeryToken]
    public Task<IActionResult> CsvImportJournalEntryLines(IFormFile file)
        => RunCsvImport(file, "JournalEntryLines", _csvImportService.ImportJournalEntryLinesAsync);

    /// <summary>
    /// Shared plumbing for the accounting CSV imports: validates the upload, resolves the company,
    /// runs the given import function, logs it, and returns the standard JSON result shape.
    /// </summary>
    private async Task<IActionResult> RunCsvImport(IFormFile file, string label,
        Func<Stream, int, Task<CsvImportResultDto>> import)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing {Label} from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, label, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await import(stream, companyId.Value);
            await LogCsvImportAsync(label, file.FileName, result);

            return Json(new
            {
                success      = result.Success,
                message      = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount   = result.ErrorCount,
                totalRows    = result.TotalRows,
                errors       = result.Errors,
                warnings     = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing {Label} from CSV", label);
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Bulk-imports payment records from a native CSV file. Invoices are resolved by InvoiceNumber.
    /// Duplicate payments (same invoice + date + amount) are skipped. Updates the invoice AmountPaid
    /// and status after each successful row.
    /// </summary>
    // POST: Tools/CsvImportPayments
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportPayments(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing payments from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportPaymentsAsync(stream, companyId.Value);
            await LogCsvImportAsync("Payments", file.FileName, result);

            return Json(new
            {
                success      = result.Success,
                message      = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount   = result.ErrorCount,
                totalRows    = result.TotalRows,
                errors       = result.Errors,
                warnings     = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing payments from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native purchase order bulk import.
    /// Columns match the native ExportPurchaseOrdersCsv output for round-trip compatibility.
    /// </summary>
    // GET: Tools/DownloadPurchaseOrderTemplate
    [HttpGet]
    public IActionResult DownloadPurchaseOrderTemplate()
    {
        var csvBytes = _csvImportService.GeneratePurchaseOrderTemplate();
        return File(csvBytes, "text/csv", "purchase_order_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports purchase order headers from a native CSV file. Vendors are resolved by
    /// company name. Existing POs matched by PoNumber are updated; new ones are created.
    /// </summary>
    // POST: Tools/CsvImportPurchaseOrders
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportPurchaseOrders(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing purchase orders from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportPurchaseOrdersAsync(stream, companyId.Value);
            await LogCsvImportAsync("PurchaseOrders", file.FileName, result);

            return Json(new
            {
                success      = result.Success,
                message      = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount   = result.ErrorCount,
                totalRows    = result.TotalRows,
                errors       = result.Errors,
                warnings     = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing purchase orders from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native appointment bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportAppointments"/>.
    /// </summary>
    // GET: Tools/DownloadAppointmentTemplate
    [HttpGet]
    public IActionResult DownloadAppointmentTemplate()
    {
        var csvBytes = _csvImportService.GenerateAppointmentTemplate();
        return File(csvBytes, "text/csv", "appointment_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports customer appointments from a native CSV file. Appointment status and type
    /// are lookup-table entities resolved by the import service against the company's current
    /// lookup data. Returns JSON with per-row status detail.
    /// </summary>
    // POST: Tools/CsvImportAppointments
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportAppointments(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing appointments from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportAppointmentsAsync(stream, companyId.Value);
            await LogCsvImportAsync("Appointments", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing appointments from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native equipment bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportEquipment"/>.
    /// </summary>
    // GET: Tools/DownloadEquipmentTemplate
    [HttpGet]
    public IActionResult DownloadEquipmentTemplate()
    {
        var csvBytes = _csvImportService.GenerateEquipmentTemplate();
        return File(csvBytes, "text/csv", "equipment_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports shop equipment records from a native CSV file. Equipment status is an enum
    /// (<c>EquipmentStatus</c>) rather than a lookup-table entity, so the import service parses
    /// the string value to the enum directly. Returns JSON with per-row status detail.
    /// </summary>
    // POST: Tools/CsvImportEquipment
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportEquipment(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing equipment from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportEquipmentAsync(stream, companyId.Value);
            await LogCsvImportAsync("Equipment", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing equipment from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native maintenance-record bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportMaintenance"/>.
    /// </summary>
    // GET: Tools/DownloadMaintenanceTemplate
    [HttpGet]
    public IActionResult DownloadMaintenanceTemplate()
    {
        var csvBytes = _csvImportService.GenerateMaintenanceTemplate();
        return File(csvBytes, "text/csv", "maintenance_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports equipment maintenance records from a native CSV file. Each record is linked
    /// to an equipment item resolved by name or number; unmatched equipment names are reported
    /// as row-level errors in the returned JSON. Returns per-row status detail for the UI accordion.
    /// </summary>
    // POST: Tools/CsvImportMaintenance
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportMaintenance(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing maintenance records from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportMaintenanceAsync(stream, companyId.Value);
            await LogCsvImportAsync("MaintenanceRecords", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing maintenance records from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native vendor bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportVendors"/>.
    /// </summary>
    // GET: Tools/DownloadVendorTemplate
    [HttpGet]
    public IActionResult DownloadVendorTemplate()
    {
        var csvBytes = _csvImportService.GenerateVendorTemplate();
        return File(csvBytes, "text/csv", "vendor_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports vendor records from a native CSV file, creating or updating vendors for the
    /// current company. Returns JSON with per-row status detail for the UI accordion.
    /// </summary>
    // POST: Tools/CsvImportVendors
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportVendors(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing vendors from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportVendorsAsync(stream, companyId.Value);
            await LogCsvImportAsync("Vendors", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing vendors from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a blank CSV template for the native prep-service bulk import, with headers
    /// matching the columns expected by <see cref="CsvImportPrepServices"/>.
    /// </summary>
    // GET: Tools/DownloadPrepServiceTemplate
    [HttpGet]
    public IActionResult DownloadPrepServiceTemplate()
    {
        var csvBytes = _csvImportService.GeneratePrepServiceTemplate();
        return File(csvBytes, "text/csv", "prep_service_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports prep-service definitions (sandblasting, masking, etc.) from a native CSV file.
    /// Prep services are used as line items on quotes and jobs; importing them saves manual entry
    /// when onboarding a company with an established service list. Returns JSON with per-row
    /// status detail.
    /// </summary>
    // POST: Tools/CsvImportPrepServices
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportPrepServices(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing prep services from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportPrepServicesAsync(stream, companyId.Value);
            await LogCsvImportAsync("PrepServices", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing prep services from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    /// <summary>
    /// Downloads a structured CSV template for company settings import. The template uses a
    /// multi-section format: sections marked with square brackets (e.g., "[Company Info]") switch
    /// the parser between key-value mode (single settings) and table mode (lookup tables such as
    /// Pricing Tiers, Job Statuses). This dual-mode format is why the file is generated inline
    /// rather than served as a static asset — the structure documents the exact expected schema.
    /// </summary>
    // GET: Tools/DownloadCompanySettingsTemplate
    [HttpGet]
    public IActionResult DownloadCompanySettingsTemplate()
    {
        try
        {
            var csvContent = GenerateCompanySettingsTemplate();
            var fileName = $"company_settings_template_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
            return File(System.Text.Encoding.UTF8.GetBytes(csvContent), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error generating company settings template");
            TempData["ErrorMessage"] = "An error occurred while generating the template.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Imports company settings from a multi-section CSV file (see <see cref="DownloadCompanySettingsTemplate"/>
    /// for format). Parsing and upsert logic are handled by <see cref="ImportCompanySettingsFromCsv"/>;
    /// this action is responsible only for file validation, stream setup, and converting the result
    /// to a JSON response. Returns a list of non-fatal warnings alongside the success flag so users
    /// can see which settings were skipped or defaulted without treating the entire import as failed.
    /// </summary>
    // POST: Tools/CsvImportCompanySettings
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportCompanySettings(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                return Json(new { success = false, message = "Your account is not associated with a company." });
            }

            if (file == null || file.Length == 0)
            {
                return Json(new { success = false, message = "No file provided or file is empty." });
            }

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
            {
                return Json(new { success = false, message = "Only CSV files are allowed." });
            }

            _logger.LogInformation("User {UserName} importing company settings from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            using var reader = new System.IO.StreamReader(stream);

            var result = await ImportCompanySettingsFromCsv(reader, companyId.Value);
            await _auditService.LogAsync("Imported", "CompanySettings", file.FileName,
                new { file.FileName, result.Success, Errors = result.Errors.Count });

            return Json(new
            {
                success = result.Success,
                message = result.Message,
                errors = result.Errors
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing company settings from CSV");
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }

    #endregion

    #region CSV Export

    /// <summary>
    /// Exports all exportable entity types (customers, quotes, jobs, appointments, catalog items,
    /// inventory, equipment, maintenance records) for the current company as a single ZIP archive.
    /// Each entity type becomes a timestamped CSV file inside the archive, making it easy to
    /// restore data to a fresh tenant or hand off data to an external system. The ZIP is built
    /// entirely in memory using <see cref="System.IO.Compression.ZipArchive"/> so no temporary
    /// files are written to disk.
    /// </summary>
    // GET: Tools/ExportAllCsv
    [HttpGet]
    public async Task<IActionResult> ExportAllCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["Error"] = "Unable to determine company context.";
                return RedirectToAction(nameof(Index));
            }

            var timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");

            using var memoryStream = new System.IO.MemoryStream();
            using (var archive = new System.IO.Compression.ZipArchive(memoryStream, System.IO.Compression.ZipArchiveMode.Create, true))
            {
                // 1. Customers
                var customers = await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId.Value);
                var customersCsv = GenerateCustomersCsv(customers);
                var customersEntry = archive.CreateEntry($"customers_{timestamp}.csv");
                using (var entryStream = customersEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(customersCsv);
                }

                // 2. Quotes
                var quotes = await _unitOfWork.Quotes.FindAsync(q => q.CompanyId == companyId.Value, false, q => q.Customer, q => q.QuoteStatus);
                var quotesCsv = GenerateQuotesCsv(quotes);
                var quotesEntry = archive.CreateEntry($"quotes_{timestamp}.csv");
                using (var entryStream = quotesEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(quotesCsv);
                }

                // 3. Jobs
                var jobs = await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId.Value, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority);
                var jobsCsv = GenerateJobsCsv(jobs);
                var jobsEntry = archive.CreateEntry($"jobs_{timestamp}.csv");
                using (var entryStream = jobsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(jobsCsv);
                }

                // 4. Appointments
                var appointments = await _unitOfWork.Appointments.FindAsync(a => a.CompanyId == companyId.Value, false,
                    a => a.Customer, a => a.AppointmentType, a => a.AppointmentStatus);
                var appointmentsCsv = GenerateAppointmentsCsv(appointments);
                var appointmentsEntry = archive.CreateEntry($"appointments_{timestamp}.csv");
                using (var entryStream = appointmentsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(appointmentsCsv);
                }

                // 5. Catalog
                var catalogCategories = await _unitOfWork.CatalogCategories.FindAsync(cc => cc.CompanyId == companyId.Value);
                var catalogCategoryPaths = BuildCategoryPathMap(catalogCategories);
                var catalog = await _unitOfWork.CatalogItems.FindAsync(ci => ci.CompanyId == companyId.Value);
                var catalogCsv = GenerateCatalogCsv(catalog, catalogCategoryPaths);
                var catalogEntry = archive.CreateEntry($"catalog_{timestamp}.csv");
                using (var entryStream = catalogEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(catalogCsv);
                }

                // 6. Inventory
                var inventory = await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId.Value);
                var inventoryCsv = GenerateInventoryCsv(inventory);
                var inventoryEntry = archive.CreateEntry($"inventory_{timestamp}.csv");
                using (var entryStream = inventoryEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(inventoryCsv);
                }

                // 7. Equipment
                var equipment = await _unitOfWork.Equipment.FindAsync(e => e.CompanyId == companyId.Value);
                var equipmentCsv = GenerateEquipmentCsv(equipment);
                var equipmentEntry = archive.CreateEntry($"equipment_{timestamp}.csv");
                using (var entryStream = equipmentEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(equipmentCsv);
                }

                // 8. Maintenance
                var maintenance = await _unitOfWork.MaintenanceRecords.FindAsync(m => m.CompanyId == companyId.Value, false, m => m.Equipment);
                var maintenanceCsv = GenerateMaintenanceCsv(maintenance);
                var maintenanceEntry = archive.CreateEntry($"maintenance_{timestamp}.csv");
                using (var entryStream = maintenanceEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(maintenanceCsv);
                }

                // 9. Vendors
                var vendors = await _unitOfWork.Vendors.FindAsync(v => v.CompanyId == companyId.Value);
                var vendorsCsv = GenerateVendorsCsv(vendors);
                var vendorsEntry = archive.CreateEntry($"vendors_{timestamp}.csv");
                using (var entryStream = vendorsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(vendorsCsv);
                }

                // 10. Prep Services
                var prepServices = await _unitOfWork.PrepServices.FindAsync(ps => ps.CompanyId == companyId.Value);
                var prepServicesCsv = GeneratePrepServicesCsv(prepServices);
                var prepServicesEntry = archive.CreateEntry($"prep_services_{timestamp}.csv");
                using (var entryStream = prepServicesEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(prepServicesCsv);
                }

                // 11. Invoices
                var invoices = await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId.Value, false, i => i.Customer, i => i.Job, i => i.InvoiceItems);
                var invoicesCsv = GenerateInvoicesCsv(invoices);
                var invoicesEntry = archive.CreateEntry($"invoices_{timestamp}.csv");
                using (var entryStream = invoicesEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(invoicesCsv);
                }

                // 12. Chart of Accounts
                var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value);
                var accountsCsv = GenerateChartOfAccountsCsv(accounts);
                var accountsEntry = archive.CreateEntry($"chart_of_accounts_{timestamp}.csv");
                using (var entryStream = accountsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(accountsCsv);
                }

                // 12b. Invoice line items — one row per line, carrying the revenue account number so
                // the invoice's revenue attribution survives an export/import round-trip.
                var accountNumberById = accounts.ToDictionary(a => a.Id, a => a.AccountNumber ?? "");
                var invoiceItemsCsv = GenerateInvoiceItemsCsv(invoices, accountNumberById);
                var invoiceItemsEntry = archive.CreateEntry($"invoice_items_{timestamp}.csv");
                using (var entryStream = invoiceItemsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(invoiceItemsCsv);
                }

                // 13. Expenses
                var expenses = await _unitOfWork.Expenses.FindAsync(e => e.CompanyId == companyId.Value, false, e => e.ExpenseAccount, e => e.PaymentAccount, e => e.Vendor, e => e.Job);
                var expensesCsv = GenerateExpensesCsv(expenses);
                var expensesEntry = archive.CreateEntry($"expenses_{timestamp}.csv");
                using (var entryStream = expensesEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(expensesCsv);
                }

                // 14. Payments
                var payments = await _unitOfWork.Payments.FindAsync(p => p.CompanyId == companyId.Value, false, p => p.Invoice, p => p.DepositAccount);
                var paymentsCsv = GeneratePaymentsCsv(payments);
                var paymentsEntry = archive.CreateEntry($"payments_{timestamp}.csv");
                using (var entryStream = paymentsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(paymentsCsv);
                }

                // 15. Bills + bill line items (account/job by number, AP account, vendor by name)
                var jobNumberById = jobs.Where(j => !string.IsNullOrEmpty(j.JobNumber)).ToDictionary(j => j.Id, j => j.JobNumber);
                var bills = await _unitOfWork.Bills.FindAsync(b => b.CompanyId == companyId.Value, false, b => b.Vendor, b => b.APAccount, b => b.LineItems);
                var billsEntry = archive.CreateEntry($"bills_{timestamp}.csv");
                using (var entryStream = billsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(GenerateBillsCsv(bills));
                }
                var billLineItemsEntry = archive.CreateEntry($"bill_line_items_{timestamp}.csv");
                using (var entryStream = billLineItemsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(GenerateBillLineItemsCsv(bills, accountNumberById, jobNumberById));
                }

                // 16. Deposits (customer by name, bank account + applied invoice by number)
                var deposits = await _unitOfWork.Deposits.FindAsync(d => d.CompanyId == companyId.Value, false, d => d.Customer, d => d.AppliedToInvoice);
                var depositsEntry = archive.CreateEntry($"deposits_{timestamp}.csv");
                using (var entryStream = depositsEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(GenerateDepositsCsv(deposits, accountNumberById));
                }

                // 17. Journal entries + lines (account by number, debit/credit)
                var journalEntries = await _unitOfWork.JournalEntries.FindAsync(j => j.CompanyId == companyId.Value, false, j => j.Lines);
                var journalEntriesEntry = archive.CreateEntry($"journal_entries_{timestamp}.csv");
                using (var entryStream = journalEntriesEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(GenerateJournalEntriesCsv(journalEntries));
                }
                var journalEntryLinesEntry = archive.CreateEntry($"journal_entry_lines_{timestamp}.csv");
                using (var entryStream = journalEntryLinesEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(GenerateJournalEntryLinesCsv(journalEntries, accountNumberById));
                }

                // 15. Purchase Orders
                var purchaseOrders = await _unitOfWork.PurchaseOrders.FindAsync(po => po.CompanyId == companyId, false, po => po.Vendor);
                var purchaseOrdersCsv = GeneratePurchaseOrdersCsv(purchaseOrders);
                var purchaseOrdersEntry = archive.CreateEntry($"purchase_orders_{timestamp}.csv");
                using (var entryStream = purchaseOrdersEntry.Open())
                using (var writer = new System.IO.StreamWriter(entryStream))
                {
                    await writer.WriteAsync(purchaseOrdersCsv);
                }

                // 16. Company Settings
                var settingsCompany = await _unitOfWork.Companies.GetByIdAsync(companyId.Value, false,
                    c => c.OperatingCosts, c => c.Preferences, c => c.PricingTiers);
                if (settingsCompany != null)
                {
                    var settingsJobStatuses       = await _unitOfWork.JobStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
                    var settingsJobPriorities      = await _unitOfWork.JobPriorityLookups.FindAsync(p => p.CompanyId == companyId.Value);
                    var settingsQuoteStatuses      = await _unitOfWork.QuoteStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
                    var settingsInventoryCategories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId.Value);
                    var settingsApptStatuses       = await _unitOfWork.AppointmentStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
                    var settingsApptTypes          = await _unitOfWork.AppointmentTypeLookups.FindAsync(t => t.CompanyId == companyId.Value);
                    var settingsCsv = GenerateCompanySettingsCsv(settingsCompany, settingsJobStatuses, settingsJobPriorities,
                        settingsQuoteStatuses, settingsInventoryCategories, settingsApptStatuses, settingsApptTypes);
                    var settingsEntry = archive.CreateEntry($"company_settings_{timestamp}.csv");
                    using (var entryStream = settingsEntry.Open())
                    using (var writer = new System.IO.StreamWriter(entryStream))
                    {
                        await writer.WriteAsync(settingsCsv);
                    }
                }
            }

            memoryStream.Position = 0;
            var zipFileName = $"all_data_export_{timestamp}.zip";

            await LogExportAsync("AllData", "Full CSV ZIP export");
            return File(memoryStream.ToArray(), "application/zip", zipFileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting all CSV data");
            TempData["Error"] = "An error occurred while exporting data.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all customers for the current company as a standalone CSV file using the native
    /// (non-QuickBooks) column layout defined in <see cref="GenerateCustomersCsv"/>. Use
    /// <see cref="ExportCustomers"/> for QuickBooks IIF/CSV export.
    /// </summary>
    // GET: Tools/ExportCustomersCsv
    [HttpGet]
    public async Task<IActionResult> ExportCustomersCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var customers = await _unitOfWork.Customers.FindAsync(c => c.CompanyId == companyId, false, c => c.PricingTier);
            var csv = GenerateCustomersCsv(customers);
            var fileName = $"customers_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Customers", $"CSV export ({customers.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting customers to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting customers.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all quotes for the current company as a CSV file. Eagerly loads Customer and
    /// QuoteStatus navigation properties so <see cref="GenerateQuotesCsv"/> can output human-readable
    /// names without additional round-trips. Prospect quotes (no linked customer) are handled by
    /// falling back to ProspectCompanyName or ProspectContactName.
    /// </summary>
    // GET: Tools/ExportQuotesCsv
    [HttpGet]
    public async Task<IActionResult> ExportQuotesCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var quotes = await _unitOfWork.Quotes.FindAsync(q => q.CompanyId == companyId, false, q => q.Customer, q => q.QuoteStatus);
            var csv = GenerateQuotesCsv(quotes);
            var fileName = $"quotes_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Quotes", $"CSV export ({quotes.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting quotes to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting quotes.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all jobs for the current company as a CSV file. Eagerly loads Customer,
    /// JobStatus, and JobPriority navigation properties because job status and priority are
    /// lookup-table entities (not enums) and must be resolved to display names for the export.
    /// </summary>
    // GET: Tools/ExportJobsCsv
    [HttpGet]
    public async Task<IActionResult> ExportJobsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var jobs = await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId, false, j => j.Customer, j => j.JobStatus, j => j.JobPriority);
            var csv = GenerateJobsCsv(jobs);
            var fileName = $"jobs_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Jobs", $"CSV export ({jobs.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting jobs to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting jobs.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all appointments for the current company as a CSV file, including appointment type
    /// and status display names resolved from their respective lookup-table navigation properties.
    /// </summary>
    // GET: Tools/ExportAppointmentsCsv
    [HttpGet]
    public async Task<IActionResult> ExportAppointmentsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var appointments = await _unitOfWork.Appointments.FindAsync(a => a.CompanyId == companyId, false,
                a => a.Customer, a => a.AppointmentType, a => a.AppointmentStatus);
            var csv = GenerateAppointmentsCsv(appointments);
            var fileName = $"appointments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";
            await LogExportAsync("Appointments", $"CSV export ({appointments.Count()} records)");

            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting appointments to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting appointments.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all catalog items for the current company as a CSV file. Catalog categories form a
    /// hierarchy (parent/child), so <see cref="BuildCategoryPathMap"/> is called first to resolve
    /// each category's full slash-separated path (e.g., "Metal/Automotive/Wheels") before building
    /// the CSV rows, making the export reimportable without needing separate category IDs.
    /// </summary>
    // GET: Tools/ExportCatalogCsv
    [HttpGet]
    public async Task<IActionResult> ExportCatalogCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var catalogCategories = await _unitOfWork.CatalogCategories.FindAsync(cc => cc.CompanyId == companyId.Value);
            var catalogCategoryPaths = BuildCategoryPathMap(catalogCategories);
            var catalogItems = await _unitOfWork.CatalogItems.FindAsync(ci => ci.CompanyId == companyId.Value);
            var csv = GenerateCatalogCsv(catalogItems, catalogCategoryPaths);
            var fileName = $"catalog_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("CatalogItems", $"CSV export ({catalogItems.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting catalog to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting catalog.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all inventory items for the current company as a CSV file including current
    /// on-hand quantities, unit costs, and reorder points, enabling offline stock analysis or
    /// re-import to a fresh environment.
    /// </summary>
    // GET: Tools/ExportInventoryCsv
    [HttpGet]
    public async Task<IActionResult> ExportInventoryCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var inventoryItems = await _unitOfWork.InventoryItems.FindAsync(i => i.CompanyId == companyId, false, i => i.PrimaryVendor);
            var csv = GenerateInventoryCsv(inventoryItems);
            var fileName = $"inventory_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("InventoryItems", $"CSV export ({inventoryItems.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting inventory to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting inventory.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all equipment records for the current company as a CSV file. Equipment status is
    /// written as its enum name (e.g., "Operational") so the CSV is human-readable and can be
    /// re-imported by the native CSV import which parses it back to the enum.
    /// </summary>
    // GET: Tools/ExportEquipmentCsv
    [HttpGet]
    public async Task<IActionResult> ExportEquipmentCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var equipment = await _unitOfWork.Equipment.FindAsync(e => e.CompanyId == companyId.Value);
            var csv = GenerateEquipmentCsv(equipment);
            var fileName = $"equipment_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Equipment", $"CSV export ({equipment.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting equipment to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting equipment.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all maintenance records for the current company as a CSV file. Note that the
    /// repository call does not eagerly load the <c>Equipment</c> navigation property, so
    /// <see cref="GenerateMaintenanceCsv"/> relies on EF lazy loading (or an already-tracked entity)
    /// to resolve equipment names. If lazy loading is disabled this column will be empty.
    /// </summary>
    // GET: Tools/ExportMaintenanceCsv
    [HttpGet]
    public async Task<IActionResult> ExportMaintenanceCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var maintenance = await _unitOfWork.MaintenanceRecords.FindAsync(m => m.CompanyId == companyId, false, m => m.Equipment);
            var csv = GenerateMaintenanceCsv(maintenance);
            var fileName = $"maintenance_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("MaintenanceRecords", $"CSV export ({maintenance.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting maintenance to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting maintenance records.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports the full company configuration — operating costs, preferences, pricing tiers, and
    /// all status/priority/category lookup tables — as a structured CSV file that can be re-imported
    /// via <see cref="CsvImportCompanySettings"/>. Lookup tables (job statuses, job priorities, etc.)
    /// are loaded separately from the company entity because they are not navigation properties on
    /// <c>Company</c>; the multi-tenancy global query filter scopes them to the current company
    /// automatically.
    /// </summary>
    // GET: Tools/ExportCompanySettingsCsv
    [HttpGet]
    public async Task<IActionResult> ExportCompanySettingsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var company = await _unitOfWork.Companies.GetByIdAsync(companyId.Value, false,
                c => c.OperatingCosts,
                c => c.Preferences,
                c => c.PricingTiers);

            if (company == null)
            {
                TempData["ErrorMessage"] = "Company not found.";
                return RedirectToAction(nameof(Index));
            }

            // Load all lookup tables — scoped to this company
            var jobStatuses = await _unitOfWork.JobStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
            var jobPriorities = await _unitOfWork.JobPriorityLookups.FindAsync(p => p.CompanyId == companyId.Value);
            var quoteStatuses = await _unitOfWork.QuoteStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
            var inventoryCategories = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId.Value);
            var appointmentStatuses = await _unitOfWork.AppointmentStatusLookups.FindAsync(s => s.CompanyId == companyId.Value);
            var appointmentTypes = await _unitOfWork.AppointmentTypeLookups.FindAsync(t => t.CompanyId == companyId.Value);

            var csv = GenerateCompanySettingsCsv(company, jobStatuses, jobPriorities,
                quoteStatuses, inventoryCategories, appointmentStatuses, appointmentTypes);
            var fileName = $"company_settings_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("CompanySettings", "CSV export");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting company settings to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting company settings.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all invoices for the current company as a CSV file, including customer name and
    /// linked job number resolved via eager loading. Invoice status is written as its enum name
    /// (e.g., "PartiallyPaid") so the data can be consumed by external accounting or reporting tools.
    /// </summary>
    // GET: Tools/ExportInvoicesCsv
    [HttpGet]
    public async Task<IActionResult> ExportInvoicesCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var invoices = await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId, false, i => i.Customer, i => i.Job);
            var csv = GenerateInvoicesCsv(invoices);
            var fileName = $"invoices_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Invoices", $"CSV export ({invoices.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting invoices to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting invoices.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all invoice payment records for the current company as a CSV file, including the
    /// parent invoice number resolved via eager loading. Payment method is written as its enum name.
    /// This export covers only customer payments (AR); vendor bill payments are not included here.
    /// </summary>
    // GET: Tools/ExportPaymentsCsv
    [HttpGet]
    public async Task<IActionResult> ExportPaymentsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var payments = await _unitOfWork.Payments.FindAsync(p => p.CompanyId == companyId, false, p => p.Invoice, p => p.DepositAccount);
            var csv = GeneratePaymentsCsv(payments);
            var fileName = $"payments_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Payments", $"CSV export ({payments.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting payments to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting payments.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all invoice line items for the current company as a CSV, keyed by parent invoice number
    /// and carrying each line's revenue account number. Complements <see cref="ExportInvoicesCsv"/>
    /// (which is header-only) so invoice detail and revenue attribution round-trip on re-import.
    /// </summary>
    // GET: Tools/ExportInvoiceItemsCsv
    [HttpGet]
    public async Task<IActionResult> ExportInvoiceItemsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var invoices = await _unitOfWork.Invoices.FindAsync(i => i.CompanyId == companyId.Value, false, i => i.InvoiceItems);
            var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value);
            var accountNumberById = accounts.ToDictionary(a => a.Id, a => a.AccountNumber ?? "");
            var csv = GenerateInvoiceItemsCsv(invoices, accountNumberById);
            var fileName = $"invoice_items_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("InvoiceItems", $"CSV export ({invoices.Sum(i => i.InvoiceItems.Count)} line items)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting invoice items to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting invoice items.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>Exports vendor bill headers (vendor by name, AP account by number) as CSV.</summary>
    // GET: Tools/ExportBillsCsv
    [HttpGet]
    public async Task<IActionResult> ExportBillsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null) { TempData["ErrorMessage"] = "Your account is not associated with a company."; return RedirectToAction(nameof(Index)); }

            var bills = await _unitOfWork.Bills.FindAsync(b => b.CompanyId == companyId.Value, false, b => b.Vendor, b => b.APAccount);
            var csv = GenerateBillsCsv(bills);
            await LogExportAsync("Bills", $"CSV export ({bills.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", $"bills_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting bills to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting bills.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>Exports vendor bill line items (account/job by number) as CSV.</summary>
    // GET: Tools/ExportBillLineItemsCsv
    [HttpGet]
    public async Task<IActionResult> ExportBillLineItemsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null) { TempData["ErrorMessage"] = "Your account is not associated with a company."; return RedirectToAction(nameof(Index)); }

            var bills = await _unitOfWork.Bills.FindAsync(b => b.CompanyId == companyId.Value, false, b => b.LineItems);
            var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value);
            var jobs = await _unitOfWork.Jobs.FindAsync(j => j.CompanyId == companyId.Value);
            var accountNumberById = accounts.ToDictionary(a => a.Id, a => a.AccountNumber ?? "");
            var jobNumberById = jobs.Where(j => !string.IsNullOrEmpty(j.JobNumber)).ToDictionary(j => j.Id, j => j.JobNumber);
            var csv = GenerateBillLineItemsCsv(bills, accountNumberById, jobNumberById);
            await LogExportAsync("BillLineItems", $"CSV export ({bills.Sum(b => b.LineItems.Count)} line items)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", $"bill_line_items_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting bill line items to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting bill line items.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>Exports customer deposits (customer by name, bank account + applied invoice by number) as CSV.</summary>
    // GET: Tools/ExportDepositsCsv
    [HttpGet]
    public async Task<IActionResult> ExportDepositsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null) { TempData["ErrorMessage"] = "Your account is not associated with a company."; return RedirectToAction(nameof(Index)); }

            var deposits = await _unitOfWork.Deposits.FindAsync(d => d.CompanyId == companyId.Value, false, d => d.Customer, d => d.AppliedToInvoice);
            var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value);
            var accountNumberById = accounts.ToDictionary(a => a.Id, a => a.AccountNumber ?? "");
            var csv = GenerateDepositsCsv(deposits, accountNumberById);
            await LogExportAsync("Deposits", $"CSV export ({deposits.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", $"deposits_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting deposits to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting deposits.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>Exports journal entry headers as CSV. Lines export separately.</summary>
    // GET: Tools/ExportJournalEntriesCsv
    [HttpGet]
    public async Task<IActionResult> ExportJournalEntriesCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null) { TempData["ErrorMessage"] = "Your account is not associated with a company."; return RedirectToAction(nameof(Index)); }

            var entries = await _unitOfWork.JournalEntries.FindAsync(j => j.CompanyId == companyId.Value);
            var csv = GenerateJournalEntriesCsv(entries);
            await LogExportAsync("JournalEntries", $"CSV export ({entries.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", $"journal_entries_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting journal entries to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting journal entries.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>Exports journal entry lines (account by number, debit/credit) as CSV.</summary>
    // GET: Tools/ExportJournalEntryLinesCsv
    [HttpGet]
    public async Task<IActionResult> ExportJournalEntryLinesCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null) { TempData["ErrorMessage"] = "Your account is not associated with a company."; return RedirectToAction(nameof(Index)); }

            var entries = await _unitOfWork.JournalEntries.FindAsync(j => j.CompanyId == companyId.Value, false, j => j.Lines);
            var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value);
            var accountNumberById = accounts.ToDictionary(a => a.Id, a => a.AccountNumber ?? "");
            var csv = GenerateJournalEntryLinesCsv(entries, accountNumberById);
            await LogExportAsync("JournalEntryLines", $"CSV export ({entries.Sum(e => e.Lines.Count)} lines)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", $"journal_entry_lines_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting journal entry lines to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting journal entry lines.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all purchase orders for the current company as a CSV file, including the vendor
    /// company name resolved via eager loading. PO status is written as its enum name.
    /// </summary>
    // GET: Tools/ExportPurchaseOrdersCsv
    [HttpGet]
    public async Task<IActionResult> ExportPurchaseOrdersCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var purchaseOrders = await _unitOfWork.PurchaseOrders.FindAsync(po => po.CompanyId == companyId, false, po => po.Vendor);
            var csv = GeneratePurchaseOrdersCsv(purchaseOrders);
            var fileName = $"purchase_orders_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("PurchaseOrders", $"CSV export ({purchaseOrders.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting purchase orders to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting purchase orders.";
            return RedirectToAction(nameof(Index));
        }
    }

    #endregion

    #region CSV Generation Helpers

    /// <summary>
    /// Builds a CSV string for the given customer collection using the native column layout that
    /// matches <see cref="ICsvImportService.ImportCustomersAsync"/>. Each field is passed through
    /// <see cref="EscapeCsv"/> to handle commas, quotes, and newlines embedded in data values.
    /// </summary>
    private string GenerateCustomersCsv(IEnumerable<Core.Entities.Customer> customers)
    {
        var sb = new System.Text.StringBuilder();
        // Column names match CustomerImportDto [Name] attributes exactly for round-trip compatibility.
        sb.AppendLine("CompanyName,ContactFirstName,ContactLastName,Email,Phone,MobilePhone,Address,City,State,ZipCode,Country,CustomerType,PricingTierCode,CreditLimit,PaymentTerms,TaxExempt,TaxId,IsActive,Notes");

        foreach (var customer in customers)
        {
            var customerType = customer.IsCommercial ? "Commercial" : "NonCommercial";
            sb.AppendLine($"{EscapeCsv(customer.CompanyName)},{EscapeCsv(customer.ContactFirstName)},{EscapeCsv(customer.ContactLastName)},{EscapeCsv(customer.Email)},{EscapeCsv(customer.Phone)},{EscapeCsv(customer.MobilePhone)},{EscapeCsv(customer.Address)},{EscapeCsv(customer.City)},{EscapeCsv(customer.State)},{EscapeCsv(customer.ZipCode)},{EscapeCsv(customer.Country)},{customerType},{EscapeCsv(customer.PricingTier?.TierName)},{customer.CreditLimit},{EscapeCsv(customer.PaymentTerms)},{customer.IsTaxExempt.ToString().ToLower()},{EscapeCsv(customer.TaxId)},{customer.IsActive.ToString().ToLower()},{EscapeCsv(customer.GeneralNotes)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given quote collection. Customer name resolution falls back to
    /// ProspectCompanyName then ProspectContactName then "Unknown" to handle quotes that were
    /// created for prospects and never converted to a linked customer record.
    /// <c>QuoteStatus.StatusCode</c> is exported (not DisplayName) so the importer can resolve it
    /// by the same code the import dict is keyed on.
    /// </summary>
    private string GenerateQuotesCsv(IEnumerable<Core.Entities.Quote> quotes)
    {
        var sb = new System.Text.StringBuilder();
        // Column names match QuoteImportDto [Name] attributes for round-trip compatibility.
        sb.AppendLine("QuoteNumber,CustomerEmail,CustomerName,ProspectCompany,ProspectContact,ProspectEmail,ProspectPhone,Status,QuoteDate,ExpirationDate,Subtotal,TaxAmount,Total,Notes,TermsAndConditions");

        foreach (var quote in quotes)
        {
            var customerName = !string.IsNullOrWhiteSpace(quote.Customer?.CompanyName)
                ? quote.Customer.CompanyName
                : $"{quote.Customer?.ContactFirstName} {quote.Customer?.ContactLastName}".Trim();
            sb.AppendLine($"{EscapeCsv(quote.QuoteNumber)},{EscapeCsv(quote.Customer?.Email)},{EscapeCsv(customerName)},{EscapeCsv(quote.ProspectCompanyName)},{EscapeCsv(quote.ProspectContactName)},{EscapeCsv(quote.ProspectEmail)},{EscapeCsv(quote.ProspectPhone)},{EscapeCsv(quote.QuoteStatus?.StatusCode)},{quote.QuoteDate:yyyy-MM-dd},{quote.ExpirationDate?.ToString("yyyy-MM-dd")},{quote.SubTotal},{quote.TaxAmount},{quote.Total},{EscapeCsv(quote.Notes)},{EscapeCsv(quote.Terms)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given job collection. JobStatus and JobPriority are lookup-table
    /// navigation properties (not enums) — their <c>StatusCode</c>/<c>PriorityCode</c> values are
    /// exported so the importer can resolve them by code (which is what the import dict is keyed on).
    /// Callers must have eagerly loaded both navigation properties before calling this helper.
    /// </summary>
    private string GenerateJobsCsv(IEnumerable<Core.Entities.Job> jobs)
    {
        var sb = new System.Text.StringBuilder();
        // Import-compatible columns first (match JobImportDto [Name] attributes), then read-only extras.
        sb.AppendLine("JobNumber,CustomerEmail,CustomerName,Status,Priority,ScheduledDate,DueDate,FinalPrice,CustomerPO,SpecialInstructions,Notes,CompletedDate,QuotedPrice,ActualTimeHours");

        foreach (var job in jobs)
        {
            var customerName = !string.IsNullOrWhiteSpace(job.Customer?.CompanyName)
                ? job.Customer.CompanyName
                : $"{job.Customer?.ContactFirstName} {job.Customer?.ContactLastName}".Trim();
            sb.AppendLine($"{EscapeCsv(job.JobNumber)},{EscapeCsv(job.Customer?.Email)},{EscapeCsv(customerName)},{EscapeCsv(job.JobStatus?.StatusCode)},{EscapeCsv(job.JobPriority?.PriorityCode)},{job.ScheduledDate?.ToString("yyyy-MM-dd")},{job.DueDate?.ToString("yyyy-MM-dd")},{job.FinalPrice},{EscapeCsv(job.CustomerPO)},{EscapeCsv(job.SpecialInstructions)},{EscapeCsv(job.InternalNotes)},{job.CompletedDate?.ToString("yyyy-MM-dd")},{job.QuotedPrice},{job.ActualTimeSpentHours}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given appointment collection. Appointment type and status are
    /// lookup-table navigation properties whose <c>DisplayName</c> is used in the export.
    /// Scheduled times are exported in ISO-8601 combined date-time format (yyyy-MM-dd HH:mm) to
    /// avoid ambiguity in locale-specific date parsing during re-import.
    /// </summary>
    private string GenerateAppointmentsCsv(IEnumerable<Core.Entities.Appointment> appointments)
    {
        var sb = new System.Text.StringBuilder();
        // Column names match AppointmentImportDto [Name] attributes for round-trip compatibility.
        sb.AppendLine("AppointmentNumber,CustomerEmail,AppointmentType,Status,ScheduledStart,ScheduledEnd,Title,Description,Location,Notes");

        foreach (var appointment in appointments)
        {
            sb.AppendLine($"{EscapeCsv(appointment.AppointmentNumber)},{EscapeCsv(appointment.Customer?.Email)},{EscapeCsv(appointment.AppointmentType?.TypeCode)},{EscapeCsv(appointment.AppointmentStatus?.StatusCode)},{appointment.ScheduledStartTime:yyyy-MM-dd HH:mm},{appointment.ScheduledEndTime:yyyy-MM-dd HH:mm},{EscapeCsv(appointment.Title)},{EscapeCsv(appointment.Description)},{EscapeCsv(appointment.Location)},{EscapeCsv(appointment.Notes)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given catalog items, using the pre-computed
    /// <paramref name="categoryPaths"/> dictionary to write the full hierarchical category path
    /// (e.g., "Metal/Automotive") rather than an opaque category ID. This makes the export
    /// directly re-importable without the user needing to re-create the category hierarchy first.
    /// </summary>
    private string GenerateCatalogCsv(IEnumerable<Core.Entities.CatalogItem> catalogItems, Dictionary<int, string> categoryPaths)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("CategoryPath,ItemName,SKU,Description,BasePrice,ApproximateArea,EstimatedMinutes,RequiresSandblasting,RequiresMasking,IsActive");

        foreach (var item in catalogItems)
        {
            var categoryPath = categoryPaths.TryGetValue(item.CategoryId, out var path) ? path : string.Empty;
            sb.AppendLine($"{EscapeCsv(categoryPath)},{EscapeCsv(item.Name)},{EscapeCsv(item.SKU)}," +
                $"{EscapeCsv(item.Description)},{item.DefaultPrice},{item.ApproximateArea}," +
                $"{item.DefaultEstimatedMinutes},{item.DefaultRequiresSandblasting}," +
                $"{item.DefaultRequiresMasking},{item.IsActive}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Converts the flat list of catalog categories into a dictionary mapping each category ID to
    /// its full slash-separated ancestor path (e.g., "Metal/Automotive/Wheels"). The algorithm
    /// walks up the <c>ParentCategoryId</c> chain for each category, prepending ancestor names,
    /// until it reaches a root category (null parent). This is O(n*d) where d is the tree depth,
    /// which is acceptable for the small category trees expected in this domain. The result is
    /// passed into <see cref="GenerateCatalogCsv"/> to avoid recomputing paths per row.
    /// </summary>
    private static Dictionary<int, string> BuildCategoryPathMap(IEnumerable<Core.Entities.CatalogCategory> categories)
    {
        var all = categories.ToDictionary(c => c.Id);
        var result = new Dictionary<int, string>();

        foreach (var category in all.Values)
        {
            var parts = new System.Collections.Generic.List<string>();
            var current = category;
            while (current != null)
            {
                parts.Insert(0, current.Name);
                current = current.ParentCategoryId.HasValue && all.TryGetValue(current.ParentCategoryId.Value, out var parent)
                    ? parent
                    : null;
            }
            result[category.Id] = string.Join("/", parts);
        }

        return result;
    }

    /// <summary>
    /// Builds a CSV string for the given inventory items using the native column layout that
    /// matches the inventory import template. Numeric fields (UnitCost, QuantityOnHand, ReorderPoint)
    /// are written without currency symbols so they can be parsed as decimals on re-import.
    /// </summary>
    private string GenerateInventoryCsv(IEnumerable<Core.Entities.InventoryItem> inventoryItems)
    {
        var sb = new System.Text.StringBuilder();
        // Column names match InventoryItemImportDto [Name] attributes for round-trip compatibility.
        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 item in inventoryItems)
        {
            sb.AppendLine($"{EscapeCsv(item.SKU)},{EscapeCsv(item.Name)},{EscapeCsv(item.Description)},{EscapeCsv(item.Category)},{EscapeCsv(item.Manufacturer)},{EscapeCsv(item.ManufacturerPartNumber)},{EscapeCsv(item.ColorName)},{EscapeCsv(item.ColorCode)},{EscapeCsv(item.Finish)},{EscapeCsv(item.PrimaryVendor?.CompanyName)},{EscapeCsv(item.VendorPartNumber)},{item.QuantityOnHand},{EscapeCsv(item.UnitOfMeasure)},{item.UnitCost},{item.LastPurchasePrice},{item.ReorderPoint},{item.ReorderQuantity},{item.MinimumStock},{item.MaximumStock},{item.CoverageSqFtPerLb},{item.TransferEfficiency},{EscapeCsv(item.Location)},{item.IsActive.ToString().ToLower()},{EscapeCsv(item.Notes)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given equipment records. <c>Status</c> is written as its enum
    /// name (e.g., "Operational", "UnderMaintenance") without special handling because the import
    /// service parses it back using <c>Enum.Parse</c> case-insensitively.
    /// </summary>
    private string GenerateEquipmentCsv(IEnumerable<Core.Entities.Equipment> equipment)
    {
        var sb = new System.Text.StringBuilder();
        // Column names match EquipmentImportDto [Name] attributes for round-trip compatibility.
        sb.AppendLine("EquipmentName,EquipmentNumber,EquipmentType,Manufacturer,Model,SerialNumber,PurchaseDate,PurchasePrice,WarrantyExpiration,Location,RecommendedMaintenanceIntervalDays,Status,IsActive,Notes");

        foreach (var item in equipment)
        {
            sb.AppendLine($"{EscapeCsv(item.EquipmentName)},{EscapeCsv(item.EquipmentNumber)},{EscapeCsv(item.EquipmentType)},{EscapeCsv(item.Manufacturer)},{EscapeCsv(item.Model)},{EscapeCsv(item.SerialNumber)},{item.PurchaseDate?.ToString("yyyy-MM-dd")},{item.PurchasePrice},{item.WarrantyExpiration?.ToString("yyyy-MM-dd")},{EscapeCsv(item.Location)},{item.RecommendedMaintenanceIntervalDays},{item.Status},{item.IsActive.ToString().ToLower()},{EscapeCsv(item.Notes)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given maintenance records. The <c>Equipment</c> navigation
    /// property is accessed directly on each record; callers must ensure it is loaded (either via
    /// eager loading or lazy loading) before invoking this helper, or the Equipment column will
    /// be null/empty in the output.
    /// </summary>
    private string GenerateMaintenanceCsv(IEnumerable<Core.Entities.MaintenanceRecord> maintenance)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("EquipmentName,MaintenanceType,ScheduledDate,CompletedDate,Status,Priority,LaborCost,PartsCost,TotalCost,Description");

        foreach (var record in maintenance)
        {
            sb.AppendLine($"{EscapeCsv(record.Equipment?.EquipmentName)},{record.MaintenanceType}," +
                $"{record.ScheduledDate:yyyy-MM-dd},{record.CompletedDate?.ToString("yyyy-MM-dd")}," +
                $"{record.Status},{record.Priority},{record.LaborCost},{record.PartsCost},{record.TotalCost},{EscapeCsv(record.Description)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a re-importable CSV for all prep services. Columns match PrepServiceImportDto
    /// so the file can be fed directly back into <see cref="CsvImportPrepServices"/>.
    /// </summary>
    private string GeneratePrepServicesCsv(IEnumerable<Core.Entities.PrepService> prepServices)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("ServiceName,Description,DisplayOrder,IsActive");

        foreach (var ps in prepServices.OrderBy(p => p.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(ps.ServiceName)},{EscapeCsv(ps.Description)},{ps.DisplayOrder},{ps.IsActive.ToString().ToLower()}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Generates a human-readable, instructional CSV template that demonstrates the multi-section
    /// format expected by <see cref="ImportCompanySettingsFromCsv"/>. Comment lines (prefixed with
    /// "#") explain the rules; section headers in square brackets switch the parser between
    /// key-value mode and table mode. Sample data rows are included so users understand the
    /// expected value format for each field without consulting separate documentation.
    /// </summary>
    private string GenerateCompanySettingsTemplate()
    {
        var sb = new System.Text.StringBuilder();

        // Header
        sb.AppendLine("# Company Settings Import Template");
        sb.AppendLine("# Instructions:");
        sb.AppendLine("#  - For single-value sections (Company Info, Operating Costs, Preferences), use Key,Value format");
        sb.AppendLine("#  - For table sections (Pricing Tiers, Lookups), use CSV table format with headers");
        sb.AppendLine("#  - Leave single-value fields blank to keep existing values unchanged");
        sb.AppendLine("#  - Table sections REPLACE all existing records - include all records you want to keep!");
        sb.AppendLine("#  - All monetary values: numbers only, no currency symbols");
        sb.AppendLine("#  - All percentages: decimal numbers (e.g., 15 for 15%)");
        sb.AppendLine("#  - Boolean values: true or false (lowercase)");
        sb.AppendLine();

        // Company Info
        sb.AppendLine("[Company Info]");
        sb.AppendLine("CompanyName,");
        sb.AppendLine("CompanyCode,");
        sb.AppendLine("PrimaryContactName,");
        sb.AppendLine("PrimaryContactEmail,");
        sb.AppendLine("Phone,");
        sb.AppendLine("Address,");
        sb.AppendLine("City,");
        sb.AppendLine("State,");
        sb.AppendLine("ZipCode,");
        sb.AppendLine("TimeZone,America/New_York");
        sb.AppendLine("AccountingMethod,Accrual");
        sb.AppendLine("TimeclockEnabled,true");
        sb.AppendLine("TimeclockAllowMultiplePunchesPerDay,true");
        sb.AppendLine("TimeclockAutoClockOutHours,");
        sb.AppendLine();

        // Operating Costs
        sb.AppendLine("[Operating Costs]");
        sb.AppendLine("StandardLaborRate,65.00");
        sb.AppendLine("LaborCostPerHour,");
        sb.AppendLine("AdditionalCoatLaborPercent,30");
        sb.AppendLine("OvenOperatingCostPerHour,25.00");
        sb.AppendLine("DefaultOvenCycleMinutes,45");
        sb.AppendLine("SandblasterCostPerHour,35.00");
        sb.AppendLine("CoatingBoothCostPerHour,30.00");
        sb.AppendLine("PowderCoatingCostPerSqFt,0.50");
        sb.AppendLine("PricingMode,MarkupOnMaterial");
        sb.AppendLine("GeneralMarkupPercentage,35");
        sb.AppendLine("TargetMarginPercent,0");
        sb.AppendLine("TaxPercent,8.5");
        sb.AppendLine("ShopSuppliesRate,5");
        sb.AppendLine("RushChargeType,Percentage");
        sb.AppendLine("RushChargePercentage,25");
        sb.AppendLine("RushChargeFixedAmount,0");
        sb.AppendLine("ShopMinimumCharge,50.00");
        sb.AppendLine("ComplexitySimplePercent,0");
        sb.AppendLine("ComplexityModeratePercent,5");
        sb.AppendLine("ComplexityComplexPercent,15");
        sb.AppendLine("ComplexityExtremePercent,25");
        sb.AppendLine("ShopCapabilityTier,Small");
        sb.AppendLine("BlastSetupType,SiphonCabinet");
        sb.AppendLine("CompressorCfm,0");
        sb.AppendLine("BlastNozzleSize,4");
        sb.AppendLine("PrimaryBlastSubstrate,Mixed");
        sb.AppendLine("BlastRateSqFtPerHourOverride,");
        sb.AppendLine("CoatingGunType,Corona");
        sb.AppendLine("CoatingRateSqFtPerHourOverride,");
        sb.AppendLine("MonthlyRent,0");
        sb.AppendLine("MonthlyUtilities,0");
        sb.AppendLine("MonthlyBillableHours,160");
        sb.AppendLine();

        // Preferences
        sb.AppendLine("[Preferences]");
        sb.AppendLine("DefaultCurrency,USD");
        sb.AppendLine("DefaultDateFormat,MM/dd/yyyy");
        sb.AppendLine("DefaultTimeFormat,12h");
        sb.AppendLine("DefaultPaymentTerms,Net 30");
        sb.AppendLine("DefaultQuoteValidityDays,30");
        sb.AppendLine("QuoteNumberPrefix,QT");
        sb.AppendLine("JobNumberPrefix,JOB");
        sb.AppendLine("InvoiceNumberPrefix,INV");
        sb.AppendLine("UseMetricSystem,false");
        sb.AppendLine("DefaultJobPriority,Normal");
        sb.AppendLine("RequireCustomerPO,false");
        sb.AppendLine("AllowCustomerApproval,true");
        sb.AppendLine("DefaultTurnaroundDays,7");
        sb.AppendLine("EmailFromAddress,");
        sb.AppendLine("EmailFromName,");
        sb.AppendLine("EmailNotificationsEnabled,true");
        sb.AppendLine("NotifyOnNewJob,true");
        sb.AppendLine("NotifyOnNewQuote,true");
        sb.AppendLine("NotifyOnJobStatusChange,true");
        sb.AppendLine("NotifyOnQuoteApproval,true");
        sb.AppendLine("NotifyOnPaymentReceived,true");
        sb.AppendLine("PaymentRemindersEnabled,false");
        sb.AppendLine("PaymentReminderDays,7,14,30");
        sb.AppendLine("QuoteExpiryWarningDays,3");
        sb.AppendLine("DueDateWarningDays,2");
        sb.AppendLine("MaintenanceAlertDays,7");
        sb.AppendLine("QuoteRetentionYears,7");
        sb.AppendLine("JobRetentionYears,7");
        sb.AppendLine("LogRetentionDays,90");
        sb.AppendLine("AutoArchiveJobsDays,365");
        sb.AppendLine("DeletedRecordRetentionDays,30");
        sb.AppendLine("QtAccentColor,#374151");
        sb.AppendLine("QtDefaultTerms,");
        sb.AppendLine("QtFooterNote,");
        sb.AppendLine("InAccentColor,#374151");
        sb.AppendLine("InDefaultTerms,");
        sb.AppendLine("InFooterNote,");
        sb.AppendLine("WoAccentColor,#374151");
        sb.AppendLine("WoTerms,");
        sb.AppendLine("KioskIntakeOutput,Quote");
        sb.AppendLine("MigratingFromQuickBooks,false");
        sb.AppendLine();

        // Pricing Tiers
        sb.AppendLine("[Pricing Tiers]");
        sb.AppendLine("TierName,Description,DiscountPercent,IsActive");
        sb.AppendLine("Standard,Standard pricing tier,0,true");
        sb.AppendLine("Silver,5% discount for regular customers,5,true");
        sb.AppendLine("Gold,10% discount for preferred customers,10,true");
        sb.AppendLine("Platinum,15% discount for VIP customers,15,true");
        sb.AppendLine();

        // Job Status Lookups
        sb.AppendLine("[Job Statuses]");
        sb.AppendLine("StatusCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive,IsSystemDefined,IsTerminalStatus,IsWorkInProgressStatus,WorkflowCategory,Description");
        sb.AppendLine("PENDING,Pending,1,secondary,bi-clock,true,true,false,false,Pre-Production,Job created but not yet started");
        sb.AppendLine("INPREPARATION,In Preparation,2,info,bi-wrench,true,false,false,true,Pre-Production,Preparing parts for coating");
        sb.AppendLine("COATING,Coating,3,primary,bi-paint-bucket,true,false,false,true,Production,Applying powder coating");
        sb.AppendLine("COMPLETED,Completed,4,success,bi-check-circle,true,true,true,false,Post-Production,Job finished");
        sb.AppendLine();

        // Job Priority Lookups
        sb.AppendLine("[Job Priorities]");
        sb.AppendLine("PriorityCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive");
        sb.AppendLine("LOW,Low,1,secondary,bi-arrow-down,true");
        sb.AppendLine("NORMAL,Normal,2,primary,bi-dash,true");
        sb.AppendLine("HIGH,High,3,warning,bi-arrow-up,true");
        sb.AppendLine("URGENT,Urgent,4,danger,bi-exclamation-triangle,true");
        sb.AppendLine();

        // Quote Status Lookups
        sb.AppendLine("[Quote Statuses]");
        sb.AppendLine("StatusCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive,IsApprovedStatus,IsConvertedStatus,IsDraftStatus");
        sb.AppendLine("DRAFT,Draft,1,secondary,bi-pencil,true,false,false,true");
        sb.AppendLine("SENT,Sent,2,info,bi-send,true,false,false,false");
        sb.AppendLine("APPROVED,Approved,3,success,bi-check-circle,true,true,false,false");
        sb.AppendLine("REJECTED,Rejected,4,danger,bi-x-circle,true,false,false,false");
        sb.AppendLine();

        // Inventory Category Lookups
        sb.AppendLine("[Inventory Categories]");
        sb.AppendLine("CategoryCode,DisplayName,DisplayOrder,IsActive,IsSystemDefined,IsCoating,Description");
        sb.AppendLine("POWDER,Powder Coating,1,true,true,true,Powder coating materials");
        sb.AppendLine("CONSUMABLES,Consumables,2,true,false,false,Shop consumables and supplies");
        sb.AppendLine();

        // Appointment Status Lookups
        sb.AppendLine("[Appointment Statuses]");
        sb.AppendLine("StatusCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive");
        sb.AppendLine("SCHEDULED,Scheduled,1,primary,bi-calendar-check,true");
        sb.AppendLine("CONFIRMED,Confirmed,2,success,bi-check2-circle,true");
        sb.AppendLine("COMPLETED,Completed,3,success,bi-check-circle,true");
        sb.AppendLine("CANCELLED,Cancelled,4,danger,bi-x-circle,true");
        sb.AppendLine();

        // Appointment Type Lookups
        sb.AppendLine("[Appointment Types]");
        sb.AppendLine("TypeCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive,Description");
        sb.AppendLine("ESTIMATE,Estimate,1,info,bi-calculator,true,On-site estimate appointment");
        sb.AppendLine("DROPOFF,Drop-off,2,primary,bi-box-arrow-in-down,true,Customer dropping off parts");
        sb.AppendLine("PICKUP,Pick-up,3,success,bi-box-arrow-up,true,Customer picking up completed job");
        sb.AppendLine();

        return sb.ToString();
    }

    /// <summary>
    /// Serialises the current company's live settings into the multi-section CSV format, producing
    /// a file that can be round-tripped back through <see cref="ImportCompanySettingsFromCsv"/>.
    /// Null OperatingCosts or Preferences sections are silently omitted so a partially configured
    /// company still produces a valid (if incomplete) export. Pricing tiers filter out soft-deleted
    /// records (<c>!t.IsDeleted</c>) because the export is intended to capture the active configuration.
    /// </summary>
    private string GenerateCompanySettingsCsv(        Core.Entities.Company company,
        IEnumerable<Core.Entities.JobStatusLookup> jobStatuses,
        IEnumerable<Core.Entities.JobPriorityLookup> jobPriorities,
        IEnumerable<Core.Entities.QuoteStatusLookup> quoteStatuses,
        IEnumerable<Core.Entities.InventoryCategoryLookup> inventoryCategories,
        IEnumerable<Core.Entities.AppointmentStatusLookup> appointmentStatuses,
        IEnumerable<Core.Entities.AppointmentTypeLookup> appointmentTypes)
    {
        var sb = new System.Text.StringBuilder();

        // Company Info
        sb.AppendLine("[Company Info]");
        sb.AppendLine($"CompanyName,{EscapeCsv(company.CompanyName)}");
        sb.AppendLine($"CompanyCode,{EscapeCsv(company.CompanyCode)}");
        sb.AppendLine($"PrimaryContactName,{EscapeCsv(company.PrimaryContactName)}");
        sb.AppendLine($"PrimaryContactEmail,{EscapeCsv(company.PrimaryContactEmail)}");
        sb.AppendLine($"Phone,{EscapeCsv(company.Phone)}");
        sb.AppendLine($"Address,{EscapeCsv(company.Address)}");
        sb.AppendLine($"City,{EscapeCsv(company.City)}");
        sb.AppendLine($"State,{EscapeCsv(company.State)}");
        sb.AppendLine($"ZipCode,{EscapeCsv(company.ZipCode)}");
        sb.AppendLine($"TimeZone,{EscapeCsv(company.TimeZone)}");
        sb.AppendLine($"AccountingMethod,{company.AccountingMethod}");
        sb.AppendLine($"TimeclockEnabled,{company.TimeclockEnabled.ToString().ToLower()}");
        sb.AppendLine($"TimeclockAllowMultiplePunchesPerDay,{company.TimeclockAllowMultiplePunchesPerDay.ToString().ToLower()}");
        sb.AppendLine($"TimeclockAutoClockOutHours,{company.TimeclockAutoClockOutHours?.ToString() ?? ""}");
        sb.AppendLine();

        // Operating Costs
        if (company.OperatingCosts != null)
        {
            var costs = company.OperatingCosts;
            sb.AppendLine("[Operating Costs]");
            sb.AppendLine($"StandardLaborRate,{costs.StandardLaborRate}");
            sb.AppendLine($"LaborCostPerHour,{costs.LaborCostPerHour?.ToString() ?? ""}");
            sb.AppendLine($"AdditionalCoatLaborPercent,{costs.AdditionalCoatLaborPercent}");
            sb.AppendLine($"OvenOperatingCostPerHour,{costs.OvenOperatingCostPerHour}");
            sb.AppendLine($"DefaultOvenCycleMinutes,{costs.DefaultOvenCycleMinutes}");
            sb.AppendLine($"SandblasterCostPerHour,{costs.SandblasterCostPerHour}");
            sb.AppendLine($"CoatingBoothCostPerHour,{costs.CoatingBoothCostPerHour}");
            sb.AppendLine($"PowderCoatingCostPerSqFt,{costs.PowderCoatingCostPerSqFt}");
            sb.AppendLine($"PricingMode,{costs.PricingMode}");
            sb.AppendLine($"GeneralMarkupPercentage,{costs.GeneralMarkupPercentage}");
            sb.AppendLine($"TargetMarginPercent,{costs.TargetMarginPercent}");
            sb.AppendLine($"TaxPercent,{costs.TaxPercent}");
            sb.AppendLine($"ShopSuppliesRate,{costs.ShopSuppliesRate}");
            sb.AppendLine($"RushChargeType,{EscapeCsv(costs.RushChargeType)}");
            sb.AppendLine($"RushChargePercentage,{costs.RushChargePercentage}");
            sb.AppendLine($"RushChargeFixedAmount,{costs.RushChargeFixedAmount}");
            sb.AppendLine($"ShopMinimumCharge,{costs.ShopMinimumCharge}");
            sb.AppendLine($"ComplexitySimplePercent,{costs.ComplexitySimplePercent}");
            sb.AppendLine($"ComplexityModeratePercent,{costs.ComplexityModeratePercent}");
            sb.AppendLine($"ComplexityComplexPercent,{costs.ComplexityComplexPercent}");
            sb.AppendLine($"ComplexityExtremePercent,{costs.ComplexityExtremePercent}");
            sb.AppendLine($"ShopCapabilityTier,{costs.ShopCapabilityTier}");
            sb.AppendLine($"BlastSetupType,{costs.BlastSetupType}");
            sb.AppendLine($"CompressorCfm,{costs.CompressorCfm}");
            sb.AppendLine($"BlastNozzleSize,{costs.BlastNozzleSize}");
            sb.AppendLine($"PrimaryBlastSubstrate,{costs.PrimaryBlastSubstrate}");
            sb.AppendLine($"BlastRateSqFtPerHourOverride,{costs.BlastRateSqFtPerHourOverride?.ToString() ?? ""}");
            sb.AppendLine($"CoatingGunType,{costs.CoatingGunType}");
            sb.AppendLine($"CoatingRateSqFtPerHourOverride,{costs.CoatingRateSqFtPerHourOverride?.ToString() ?? ""}");
            sb.AppendLine($"MonthlyRent,{costs.MonthlyRent}");
            sb.AppendLine($"MonthlyUtilities,{costs.MonthlyUtilities}");
            sb.AppendLine($"MonthlyBillableHours,{costs.MonthlyBillableHours}");
            sb.AppendLine();
        }

        // Preferences
        if (company.Preferences != null)
        {
            var prefs = company.Preferences;
            sb.AppendLine("[Preferences]");
            sb.AppendLine($"DefaultCurrency,{EscapeCsv(prefs.DefaultCurrency)}");
            sb.AppendLine($"DefaultDateFormat,{EscapeCsv(prefs.DefaultDateFormat)}");
            sb.AppendLine($"DefaultTimeFormat,{EscapeCsv(prefs.DefaultTimeFormat)}");
            sb.AppendLine($"DefaultPaymentTerms,{EscapeCsv(prefs.DefaultPaymentTerms)}");
            sb.AppendLine($"DefaultQuoteValidityDays,{prefs.DefaultQuoteValidityDays}");
            sb.AppendLine($"QuoteNumberPrefix,{EscapeCsv(prefs.QuoteNumberPrefix)}");
            sb.AppendLine($"JobNumberPrefix,{EscapeCsv(prefs.JobNumberPrefix)}");
            sb.AppendLine($"InvoiceNumberPrefix,{EscapeCsv(prefs.InvoiceNumberPrefix)}");
            sb.AppendLine($"UseMetricSystem,{prefs.UseMetricSystem.ToString().ToLower()}");
            sb.AppendLine($"DefaultJobPriority,{EscapeCsv(prefs.DefaultJobPriority)}");
            sb.AppendLine($"RequireCustomerPO,{prefs.RequireCustomerPO.ToString().ToLower()}");
            sb.AppendLine($"AllowCustomerApproval,{prefs.AllowCustomerApproval.ToString().ToLower()}");
            sb.AppendLine($"DefaultTurnaroundDays,{prefs.DefaultTurnaroundDays}");
            sb.AppendLine($"EmailFromAddress,{EscapeCsv(prefs.EmailFromAddress)}");
            sb.AppendLine($"EmailFromName,{EscapeCsv(prefs.EmailFromName)}");
            sb.AppendLine($"EmailNotificationsEnabled,{prefs.EmailNotificationsEnabled.ToString().ToLower()}");
            sb.AppendLine($"NotifyOnNewJob,{prefs.NotifyOnNewJob.ToString().ToLower()}");
            sb.AppendLine($"NotifyOnNewQuote,{prefs.NotifyOnNewQuote.ToString().ToLower()}");
            sb.AppendLine($"NotifyOnJobStatusChange,{prefs.NotifyOnJobStatusChange.ToString().ToLower()}");
            sb.AppendLine($"NotifyOnQuoteApproval,{prefs.NotifyOnQuoteApproval.ToString().ToLower()}");
            sb.AppendLine($"NotifyOnPaymentReceived,{prefs.NotifyOnPaymentReceived.ToString().ToLower()}");
            sb.AppendLine($"PaymentRemindersEnabled,{prefs.PaymentRemindersEnabled.ToString().ToLower()}");
            sb.AppendLine($"PaymentReminderDays,{EscapeCsv(prefs.PaymentReminderDays)}");
            sb.AppendLine($"QuoteExpiryWarningDays,{prefs.QuoteExpiryWarningDays}");
            sb.AppendLine($"DueDateWarningDays,{prefs.DueDateWarningDays}");
            sb.AppendLine($"MaintenanceAlertDays,{prefs.MaintenanceAlertDays}");
            sb.AppendLine($"QuoteRetentionYears,{prefs.QuoteRetentionYears}");
            sb.AppendLine($"JobRetentionYears,{prefs.JobRetentionYears}");
            sb.AppendLine($"LogRetentionDays,{prefs.LogRetentionDays}");
            sb.AppendLine($"AutoArchiveJobsDays,{prefs.AutoArchiveJobsDays}");
            sb.AppendLine($"DeletedRecordRetentionDays,{prefs.DeletedRecordRetentionDays}");
            sb.AppendLine($"QtAccentColor,{EscapeCsv(prefs.QtAccentColor)}");
            sb.AppendLine($"QtDefaultTerms,{EscapeCsv(prefs.QtDefaultTerms)}");
            sb.AppendLine($"QtFooterNote,{EscapeCsv(prefs.QtFooterNote)}");
            sb.AppendLine($"InAccentColor,{EscapeCsv(prefs.InAccentColor)}");
            sb.AppendLine($"InDefaultTerms,{EscapeCsv(prefs.InDefaultTerms)}");
            sb.AppendLine($"InFooterNote,{EscapeCsv(prefs.InFooterNote)}");
            sb.AppendLine($"WoAccentColor,{EscapeCsv(prefs.WoAccentColor)}");
            sb.AppendLine($"WoTerms,{EscapeCsv(prefs.WoTerms)}");
            sb.AppendLine($"KioskIntakeOutput,{EscapeCsv(prefs.KioskIntakeOutput)}");
            sb.AppendLine($"MigratingFromQuickBooks,{prefs.MigratingFromQuickBooks.ToString().ToLower()}");
            sb.AppendLine();
        }

        // Pricing Tiers
        sb.AppendLine("[Pricing Tiers]");
        sb.AppendLine("TierName,Description,DiscountPercent,IsActive");
        foreach (var tier in company.PricingTiers.Where(t => !t.IsDeleted).OrderBy(t => t.TierName))
        {
            sb.AppendLine($"{EscapeCsv(tier.TierName)},{EscapeCsv(tier.Description)},{tier.DiscountPercent},{tier.IsActive.ToString().ToLower()}");
        }
        sb.AppendLine();

        // Job Status Lookups
        sb.AppendLine("[Job Statuses]");
        sb.AppendLine("StatusCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive,IsSystemDefined,IsTerminalStatus,IsWorkInProgressStatus,WorkflowCategory,Description");
        foreach (var status in jobStatuses.OrderBy(s => s.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(status.StatusCode)},{EscapeCsv(status.DisplayName)},{status.DisplayOrder}," +
                $"{EscapeCsv(status.ColorClass)},{EscapeCsv(status.IconClass)},{status.IsActive.ToString().ToLower()}," +
                $"{status.IsSystemDefined.ToString().ToLower()},{status.IsTerminalStatus.ToString().ToLower()}," +
                $"{status.IsWorkInProgressStatus.ToString().ToLower()},{EscapeCsv(status.WorkflowCategory)},{EscapeCsv(status.Description)}");
        }
        sb.AppendLine();

        // Job Priority Lookups
        sb.AppendLine("[Job Priorities]");
        sb.AppendLine("PriorityCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive");
        foreach (var priority in jobPriorities.OrderBy(p => p.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(priority.PriorityCode)},{EscapeCsv(priority.DisplayName)},{priority.DisplayOrder}," +
                $"{EscapeCsv(priority.ColorClass)},{EscapeCsv(priority.IconClass)},{priority.IsActive.ToString().ToLower()}");
        }
        sb.AppendLine();

        // Quote Status Lookups
        sb.AppendLine("[Quote Statuses]");
        sb.AppendLine("StatusCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive,IsApprovedStatus,IsConvertedStatus,IsDraftStatus");
        foreach (var status in quoteStatuses.OrderBy(s => s.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(status.StatusCode)},{EscapeCsv(status.DisplayName)},{status.DisplayOrder}," +
                $"{EscapeCsv(status.ColorClass)},{EscapeCsv(status.IconClass)},{status.IsActive.ToString().ToLower()}," +
                $"{status.IsApprovedStatus.ToString().ToLower()},{status.IsConvertedStatus.ToString().ToLower()}," +
                $"{status.IsDraftStatus.ToString().ToLower()}");
        }
        sb.AppendLine();

        // Inventory Category Lookups
        sb.AppendLine("[Inventory Categories]");
        sb.AppendLine("CategoryCode,DisplayName,DisplayOrder,IsActive,IsSystemDefined,IsCoating,Description");
        foreach (var category in inventoryCategories.OrderBy(c => c.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(category.CategoryCode)},{EscapeCsv(category.DisplayName)},{category.DisplayOrder}," +
                $"{category.IsActive.ToString().ToLower()},{category.IsSystemDefined.ToString().ToLower()}," +
                $"{category.IsCoating.ToString().ToLower()},{EscapeCsv(category.Description)}");
        }
        sb.AppendLine();

        // Appointment Status Lookups
        sb.AppendLine("[Appointment Statuses]");
        sb.AppendLine("StatusCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive");
        foreach (var status in appointmentStatuses.OrderBy(s => s.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(status.StatusCode)},{EscapeCsv(status.DisplayName)},{status.DisplayOrder}," +
                $"{EscapeCsv(status.ColorClass)},{EscapeCsv(status.IconClass)},{status.IsActive.ToString().ToLower()}");
        }
        sb.AppendLine();

        // Appointment Type Lookups
        sb.AppendLine("[Appointment Types]");
        sb.AppendLine("TypeCode,DisplayName,DisplayOrder,ColorClass,IconClass,IsActive,Description");
        foreach (var type in appointmentTypes.OrderBy(t => t.DisplayOrder))
        {
            sb.AppendLine($"{EscapeCsv(type.TypeCode)},{EscapeCsv(type.DisplayName)},{type.DisplayOrder}," +
                $"{EscapeCsv(type.ColorClass)},{EscapeCsv(type.IconClass)},{type.IsActive.ToString().ToLower()},{EscapeCsv(type.Description)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Core parser and upsert engine for company-settings CSV import. Reads the file line by line,
    /// switching between key-value mode (for Company Info, Operating Costs, Preferences) and table
    /// mode (for Pricing Tiers and all lookup tables) based on section headers. Key-value fields
    /// that parse successfully are applied to the in-memory entity; parse errors are collected as
    /// non-fatal warnings (the row is skipped, but processing continues). Table sections are
    /// accumulated in memory, then processed after the full file is read, so the final table state
    /// reflects the entire section rather than partial rows applied mid-stream. A single
    /// <c>CompleteAsync</c> call at the end commits all changes atomically.
    /// <para>
    /// Design caveat: Empty CSV values are silently skipped for key-value rows (so a blank value
    /// means "keep existing"), but table sections REPLACE all existing records — the caller must
    /// include every row they want to retain in a table section export.
    /// </para>
    /// </summary>
    private async Task<(bool Success, string Message, List<string> Errors)> ImportCompanySettingsFromCsv(System.IO.StreamReader reader, int companyId)
    {
        var errors = new List<string>();
        var currentSection = "";
        var lineNumber = 0;
        var tableSections = new Dictionary<string, List<string[]>>();
        var tableHeaders = new Dictionary<string, string[]>();

        try
        {
            var company = await _unitOfWork.Companies.GetByIdAsync(companyId, false,
                c => c.OperatingCosts,
                c => c.Preferences,
                c => c.PricingTiers);

            if (company == null)
            {
                return (false, "Company not found.", errors);
            }

            // Create operating costs if null
            if (company.OperatingCosts == null)
            {
                company.OperatingCosts = new Core.Entities.CompanyOperatingCosts
                {
                    CompanyId = companyId
                };
            }

            // Create preferences if null
            if (company.Preferences == null)
            {
                company.Preferences = new Core.Entities.CompanyPreferences
                {
                    CompanyId = companyId
                };
            }

            // Parse the CSV
            string? line;
            bool isTableSection = false;
            bool isFirstLineOfTable = false;

            while ((line = await reader.ReadLineAsync()) != null)
            {
                lineNumber++;

                // Skip comments and empty lines
                if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#"))
                    continue;

                // Check for section headers
                if (line.StartsWith("[") && line.EndsWith("]"))
                {
                    currentSection = line.Trim('[', ']');
                    isTableSection = IsTableSection(currentSection);
                    isFirstLineOfTable = isTableSection;

                    if (isTableSection)
                    {
                        tableSections[currentSection] = new List<string[]>();
                    }
                    continue;
                }

                if (isTableSection)
                {
                    // Parse CSV row
                    var cells = ParseCsvLine(line);

                    if (isFirstLineOfTable)
                    {
                        // This is the header row
                        tableHeaders[currentSection] = cells;
                        isFirstLineOfTable = false;
                    }
                    else
                    {
                        // This is a data row
                        tableSections[currentSection].Add(cells);
                    }
                }
                else
                {
                    // Parse key-value pairs
                    var parts = line.Split(',', 2);
                    if (parts.Length != 2)
                    {
                        errors.Add($"Line {lineNumber}: Invalid format (expected Key,Value)");
                        continue;
                    }

                    var key = parts[0].Trim();
                    var value = parts[1].Trim();

                    // Skip empty values
                    if (string.IsNullOrWhiteSpace(value))
                        continue;

                    try
                    {
                        switch (currentSection)
                        {
                            case "Company Info":
                                UpdateCompanyInfo(company, key, value);
                                break;
                            case "Operating Costs":
                                UpdateOperatingCosts(company.OperatingCosts, key, value);
                                break;
                            case "Preferences":
                                UpdatePreferences(company.Preferences, key, value);
                                break;
                        }
                    }
                    catch (Exception ex)
                    {
                        errors.Add($"Line {lineNumber} ({key}): {ex.Message}");
                    }
                }
            }

            // Process table sections
            foreach (var section in tableSections)
            {
                try
                {
                    await ProcessTableSection(section.Key, tableHeaders[section.Key], section.Value, companyId, errors);
                }
                catch (Exception ex)
                {
                    errors.Add($"Section [{section.Key}]: {ex.Message}");
                }
            }

            await _unitOfWork.CompleteAsync();

            var message = errors.Any()
                ? $"Import completed with {errors.Count} warning(s). Settings have been updated."
                : "All settings imported successfully!";

            return (true, message, errors);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing company settings");
            return (false, $"Import failed: {ex.Message}", errors);
        }
    }

    /// <summary>
    /// Returns true if the given CSV section name corresponds to a multi-row table (Pricing Tiers,
    /// Job Statuses, etc.) rather than a flat key-value section (Company Info, Operating Costs,
    /// Preferences). The distinction controls whether <see cref="ImportCompanySettingsFromCsv"/>
    /// reads lines as "Key,Value" pairs or as CSV rows with a header row followed by data rows.
    /// </summary>
    private bool IsTableSection(string sectionName)
    {
        return sectionName switch
        {
            "Pricing Tiers" => true,
            "Job Statuses" => true,
            "Job Priorities" => true,
            "Quote Statuses" => true,
            "Inventory Categories" => true,
            "Appointment Statuses" => true,
            "Appointment Types" => true,
            _ => false
        };
    }

    /// <summary>
    /// Parses a single CSV line into a string array, correctly handling RFC 4180 quoting rules:
    /// fields enclosed in double quotes may contain commas, newlines, or literal double-quotes
    /// (escaped as two consecutive double-quotes ""). Each resulting cell is trimmed of leading
    /// and trailing whitespace. Used exclusively for table-section rows where a standard
    /// <c>string.Split(',')</c> would break on embedded commas in field values (e.g., addresses).
    /// </summary>
    private string[] ParseCsvLine(string line)
    {
        var cells = new List<string>();
        var inQuotes = false;
        var currentCell = new System.Text.StringBuilder();

        for (int i = 0; i < line.Length; i++)
        {
            var c = line[i];

            if (c == '"')
            {
                if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
                {
                    // Escaped quote
                    currentCell.Append('"');
                    i++;
                }
                else
                {
                    inQuotes = !inQuotes;
                }
            }
            else if (c == ',' && !inQuotes)
            {
                cells.Add(currentCell.ToString().Trim());
                currentCell.Clear();
            }
            else
            {
                currentCell.Append(c);
            }
        }

        cells.Add(currentCell.ToString().Trim());
        return cells.ToArray();
    }

    /// <summary>
    /// Dispatches table-section data to the appropriate typed import helper based on the section
    /// name. Each helper receives the parsed header row (for column-index resolution) and all data
    /// rows, and appends any row-level errors to the shared <paramref name="errors"/> list.
    /// Adding a new importable lookup table requires adding a case here and implementing the
    /// corresponding <c>Import*</c> private method.
    /// </summary>
    private async Task ProcessTableSection(string sectionName, string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        switch (sectionName)
        {
            case "Pricing Tiers":
                await ImportPricingTiers(headers, rows, companyId, errors);
                break;
            case "Job Statuses":
                await ImportJobStatuses(headers, rows, companyId, errors);
                break;
            case "Job Priorities":
                await ImportJobPriorities(headers, rows, companyId, errors);
                break;
            case "Quote Statuses":
                await ImportQuoteStatuses(headers, rows, companyId, errors);
                break;
            case "Inventory Categories":
                await ImportInventoryCategories(headers, rows, companyId, errors);
                break;
            case "Appointment Statuses":
                await ImportAppointmentStatuses(headers, rows, companyId, errors);
                break;
            case "Appointment Types":
                await ImportAppointmentTypes(headers, rows, companyId, errors);
                break;
        }
    }

    /// <summary>
    /// Applies a single key-value pair from the "[Company Info]" CSV section to the given company
    /// entity. Unrecognised keys are silently ignored so the file format can evolve without breaking
    /// older imports. The caller is responsible for saving changes via <c>CompleteAsync</c>.
    /// </summary>
    private void UpdateCompanyInfo(Core.Entities.Company company, string key, string value)
    {
        switch (key)
        {
            case "CompanyName":
                company.CompanyName = value;
                break;
            case "CompanyCode":
                company.CompanyCode = value;
                break;
            case "PrimaryContactName":
                company.PrimaryContactName = value;
                break;
            case "PrimaryContactEmail":
                company.PrimaryContactEmail = value;
                break;
            case "Phone":
                company.Phone = value;
                break;
            case "Address":
                company.Address = value;
                break;
            case "City":
                company.City = value;
                break;
            case "State":
                company.State = value;
                break;
            case "ZipCode":
                company.ZipCode = value;
                break;
            case "TimeZone":
                company.TimeZone = value;
                break;
        }
    }

    /// <summary>
    /// Applies a single key-value pair from the "[Operating Costs]" CSV section to the given
    /// operating costs entity using <c>decimal.Parse</c>. Throws <see cref="FormatException"/> on
    /// invalid numeric values; callers should catch and log this as a row-level warning without
    /// aborting the entire import. Unrecognised keys are silently ignored.
    /// </summary>
    private void UpdateOperatingCosts(Core.Entities.CompanyOperatingCosts costs, string key, string value)
    {
        switch (key)
        {
            case "StandardLaborRate":
                costs.StandardLaborRate = decimal.Parse(value);
                break;
            case "AdditionalCoatLaborPercent":
                costs.AdditionalCoatLaborPercent = decimal.Parse(value);
                break;
            case "OvenOperatingCostPerHour":
                costs.OvenOperatingCostPerHour = decimal.Parse(value);
                break;
            case "SandblasterCostPerHour":
                costs.SandblasterCostPerHour = decimal.Parse(value);
                break;
            case "CoatingBoothCostPerHour":
                costs.CoatingBoothCostPerHour = decimal.Parse(value);
                break;
            case "PowderCoatingCostPerSqFt":
                costs.PowderCoatingCostPerSqFt = decimal.Parse(value);
                break;
            case "GeneralMarkupPercentage":
                costs.GeneralMarkupPercentage = decimal.Parse(value);
                break;
            case "TaxPercent":
                costs.TaxPercent = decimal.Parse(value);
                break;
            case "ShopSuppliesRate":
                costs.ShopSuppliesRate = decimal.Parse(value);
                break;
            case "RushChargeType":
                costs.RushChargeType = value;
                break;
            case "RushChargePercentage":
                costs.RushChargePercentage = decimal.Parse(value);
                break;
            case "RushChargeFixedAmount":
                costs.RushChargeFixedAmount = decimal.Parse(value);
                break;
            case "ShopMinimumCharge":
                costs.ShopMinimumCharge = decimal.Parse(value);
                break;
        }
    }

    /// <summary>
    /// Applies a single key-value pair from the "[Preferences]" CSV section to the given
    /// preferences entity. Boolean fields use <c>bool.Parse</c> (case-insensitive, expects "true"
    /// or "false"); integer fields use <c>int.Parse</c>. Like <see cref="UpdateOperatingCosts"/>,
    /// parse failures throw and are caught by the caller as row-level warnings.
    /// </summary>
    private void UpdatePreferences(Core.Entities.CompanyPreferences prefs, string key, string value)
    {
        switch (key)
        {
            case "DefaultCurrency":
                prefs.DefaultCurrency = value;
                break;
            case "DefaultDateFormat":
                prefs.DefaultDateFormat = value;
                break;
            case "DefaultTimeFormat":
                prefs.DefaultTimeFormat = value;
                break;
            case "DefaultPaymentTerms":
                prefs.DefaultPaymentTerms = value;
                break;
            case "DefaultQuoteValidityDays":
                prefs.DefaultQuoteValidityDays = int.Parse(value);
                break;
            case "QuoteNumberPrefix":
                prefs.QuoteNumberPrefix = value;
                break;
            case "JobNumberPrefix":
                prefs.JobNumberPrefix = value;
                break;
            case "UseMetricSystem":
                prefs.UseMetricSystem = bool.Parse(value);
                break;
            case "DefaultJobPriority":
                prefs.DefaultJobPriority = value;
                break;
            case "RequireCustomerPO":
                prefs.RequireCustomerPO = bool.Parse(value);
                break;
            case "AllowCustomerApproval":
                prefs.AllowCustomerApproval = bool.Parse(value);
                break;
            case "DefaultTurnaroundDays":
                prefs.DefaultTurnaroundDays = int.Parse(value);
                break;
            case "EmailNotificationsEnabled":
                prefs.EmailNotificationsEnabled = bool.Parse(value);
                break;
            case "NotifyOnNewJob":
                prefs.NotifyOnNewJob = bool.Parse(value);
                break;
            case "NotifyOnJobStatusChange":
                prefs.NotifyOnJobStatusChange = bool.Parse(value);
                break;
            case "NotifyOnQuoteApproval":
                prefs.NotifyOnQuoteApproval = bool.Parse(value);
                break;
            case "NotifyOnPaymentReceived":
                prefs.NotifyOnPaymentReceived = bool.Parse(value);
                break;
            case "QuoteExpiryWarningDays":
                prefs.QuoteExpiryWarningDays = int.Parse(value);
                break;
            case "DueDateWarningDays":
                prefs.DueDateWarningDays = int.Parse(value);
                break;
            case "MaintenanceAlertDays":
                prefs.MaintenanceAlertDays = int.Parse(value);
                break;
            case "QuoteRetentionYears":
                prefs.QuoteRetentionYears = int.Parse(value);
                break;
            case "JobRetentionYears":
                prefs.JobRetentionYears = int.Parse(value);
                break;
            case "LogRetentionDays":
                prefs.LogRetentionDays = int.Parse(value);
                break;
            case "AutoArchiveJobsDays":
                prefs.AutoArchiveJobsDays = int.Parse(value);
                break;
            case "DeletedRecordRetentionDays":
                prefs.DeletedRecordRetentionDays = int.Parse(value);
                break;
        }
    }

    /// <summary>
    /// Replaces all existing pricing tiers for the company with the tiers defined in the CSV
    /// table section. Existing records are physically deleted (not soft-deleted) because pricing
    /// tiers are configuration data, not transactional records; any customers referencing deleted
    /// tiers will retain their FK but the tier row will be gone. Callers should warn users that
    /// table sections replace all existing records — the export/import round-trip is the safe path.
    /// Column positions are resolved by name from <paramref name="headers"/> so the import is
    /// resilient to column reordering.
    /// </summary>
    private async Task ImportPricingTiers(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing tiers for this company
        var existing = await _unitOfWork.PricingTiers.FindAsync(pt => pt.CompanyId == companyId);
        foreach (var tier in existing)
        {
            await _unitOfWork.PricingTiers.DeleteAsync(tier);
        }

        // Import new tiers
        var nameIdx = Array.IndexOf(headers, "TierName");
        var descIdx = Array.IndexOf(headers, "Description");
        var discountIdx = Array.IndexOf(headers, "DiscountPercent");
        var activeIdx = Array.IndexOf(headers, "IsActive");

        foreach (var row in rows)
        {
            try
            {
                var tier = new Core.Entities.PricingTier
                {
                    CompanyId = companyId,
                    TierName = row[nameIdx],
                    Description = descIdx >= 0 && descIdx < row.Length ? row[descIdx] : null,
                    DiscountPercent = discountIdx >= 0 && discountIdx < row.Length ? decimal.Parse(row[discountIdx]) : 0,
                    IsActive = activeIdx >= 0 && activeIdx < row.Length ? bool.Parse(row[activeIdx]) : true
                };
                await _unitOfWork.PricingTiers.AddAsync(tier);
            }
            catch (Exception ex)
            {
                errors.Add($"Pricing Tier '{row[nameIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Upserts job status lookup records from the CSV table section. Unlike other lookup importers,
    /// this one does NOT delete all existing records first: only non-system-defined statuses are
    /// deleted, and system-defined ones are updated in-place via a find-by-StatusCode lookup.
    /// This protects built-in statuses (e.g., "PENDING", "COMPLETED") that jobs may already
    /// reference, while still allowing custom statuses to be replaced. System-defined rows from
    /// the CSV are recognised by their <c>IsSystemDefined</c> column value.
    /// </summary>
    private async Task ImportJobStatuses(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing non-system-defined statuses
        var existing = await _unitOfWork.JobStatusLookups.FindAsync(s => s.CompanyId == companyId && !s.IsSystemDefined);
        foreach (var status in existing)
        {
            await _unitOfWork.JobStatusLookups.DeleteAsync(status);
        }

        // Update or add statuses
        var codeIdx = Array.IndexOf(headers, "StatusCode");
        var nameIdx = Array.IndexOf(headers, "DisplayName");
        var orderIdx = Array.IndexOf(headers, "DisplayOrder");
        var colorIdx = Array.IndexOf(headers, "ColorClass");
        var iconIdx = Array.IndexOf(headers, "IconClass");
        var activeIdx = Array.IndexOf(headers, "IsActive");
        var systemIdx = Array.IndexOf(headers, "IsSystemDefined");
        var terminalIdx = Array.IndexOf(headers, "IsTerminalStatus");
        var wipIdx = Array.IndexOf(headers, "IsWorkInProgressStatus");
        var categoryIdx = Array.IndexOf(headers, "WorkflowCategory");
        var descIdx = Array.IndexOf(headers, "Description");

        foreach (var row in rows)
        {
            try
            {
                var statusCode = row[codeIdx];
                var existingStatus = await _unitOfWork.JobStatusLookups.FindAsync(s => s.CompanyId == companyId && s.StatusCode == statusCode);
                var status = existingStatus.FirstOrDefault();

                if (status == null)
                {
                    status = new Core.Entities.JobStatusLookup { CompanyId = companyId, StatusCode = statusCode };
                    await _unitOfWork.JobStatusLookups.AddAsync(status);
                }

                status.DisplayName = row[nameIdx];
                status.DisplayOrder = int.Parse(row[orderIdx]);
                status.ColorClass = row[colorIdx];
                status.IconClass = iconIdx >= 0 && iconIdx < row.Length ? row[iconIdx] : null;
                status.IsActive = bool.Parse(row[activeIdx]);
                status.IsSystemDefined = bool.Parse(row[systemIdx]);
                status.IsTerminalStatus = bool.Parse(row[terminalIdx]);
                status.IsWorkInProgressStatus = bool.Parse(row[wipIdx]);
                status.WorkflowCategory = categoryIdx >= 0 && categoryIdx < row.Length ? row[categoryIdx] : null;
                status.Description = descIdx >= 0 && descIdx < row.Length ? row[descIdx] : null;
            }
            catch (Exception ex)
            {
                errors.Add($"Job Status '{row[codeIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Replaces all job priority lookup records for the company with the priorities defined in
    /// the CSV table section. All existing priorities are physically deleted before inserting the
    /// new set, so callers must include every priority they want to keep (including the system
    /// defaults) when submitting the import file.
    /// </summary>
    private async Task ImportJobPriorities(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing priorities
        var existing = await _unitOfWork.JobPriorityLookups.FindAsync(p => p.CompanyId == companyId);
        foreach (var priority in existing)
        {
            await _unitOfWork.JobPriorityLookups.DeleteAsync(priority);
        }

        var codeIdx = Array.IndexOf(headers, "PriorityCode");
        var nameIdx = Array.IndexOf(headers, "DisplayName");
        var orderIdx = Array.IndexOf(headers, "DisplayOrder");
        var colorIdx = Array.IndexOf(headers, "ColorClass");
        var iconIdx = Array.IndexOf(headers, "IconClass");
        var activeIdx = Array.IndexOf(headers, "IsActive");

        foreach (var row in rows)
        {
            try
            {
                var priority = new Core.Entities.JobPriorityLookup
                {
                    CompanyId = companyId,
                    PriorityCode = row[codeIdx],
                    DisplayName = row[nameIdx],
                    DisplayOrder = int.Parse(row[orderIdx]),
                    ColorClass = row[colorIdx],
                    IconClass = iconIdx >= 0 && iconIdx < row.Length ? row[iconIdx] : null,
                    IsActive = bool.Parse(row[activeIdx])
                };
                await _unitOfWork.JobPriorityLookups.AddAsync(priority);
            }
            catch (Exception ex)
            {
                errors.Add($"Job Priority '{row[codeIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Replaces all quote status lookup records for the company with the statuses defined in the
    /// CSV table section. The three semantic flags (<c>IsApprovedStatus</c>, <c>IsConvertedStatus</c>,
    /// <c>IsDraftStatus</c>) are critical for workflow logic (e.g., only an approved quote can be
    /// converted to a job), so importers must ensure exactly one row sets each flag to true.
    /// </summary>
    private async Task ImportQuoteStatuses(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing statuses
        var existing = await _unitOfWork.QuoteStatusLookups.FindAsync(s => s.CompanyId == companyId);
        foreach (var status in existing)
        {
            await _unitOfWork.QuoteStatusLookups.DeleteAsync(status);
        }

        var codeIdx = Array.IndexOf(headers, "StatusCode");
        var nameIdx = Array.IndexOf(headers, "DisplayName");
        var orderIdx = Array.IndexOf(headers, "DisplayOrder");
        var colorIdx = Array.IndexOf(headers, "ColorClass");
        var iconIdx = Array.IndexOf(headers, "IconClass");
        var activeIdx = Array.IndexOf(headers, "IsActive");
        var approvedIdx = Array.IndexOf(headers, "IsApprovedStatus");
        var convertedIdx = Array.IndexOf(headers, "IsConvertedStatus");
        var draftIdx = Array.IndexOf(headers, "IsDraftStatus");

        foreach (var row in rows)
        {
            try
            {
                var status = new Core.Entities.QuoteStatusLookup
                {
                    CompanyId = companyId,
                    StatusCode = row[codeIdx],
                    DisplayName = row[nameIdx],
                    DisplayOrder = int.Parse(row[orderIdx]),
                    ColorClass = row[colorIdx],
                    IconClass = iconIdx >= 0 && iconIdx < row.Length ? row[iconIdx] : null,
                    IsActive = bool.Parse(row[activeIdx]),
                    IsApprovedStatus = bool.Parse(row[approvedIdx]),
                    IsConvertedStatus = bool.Parse(row[convertedIdx]),
                    IsDraftStatus = bool.Parse(row[draftIdx])
                };
                await _unitOfWork.QuoteStatusLookups.AddAsync(status);
            }
            catch (Exception ex)
            {
                errors.Add($"Quote Status '{row[codeIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Replaces all inventory category lookup records for the company with the categories defined
    /// in the CSV table section. The <c>IsCoating</c> flag is used by the pricing engine to
    /// identify powder-coating material categories for coverage calculations; at least one category
    /// with <c>IsCoating=true</c> should be present for quoting to work correctly.
    /// </summary>
    private async Task ImportInventoryCategories(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing categories
        var existing = await _unitOfWork.InventoryCategoryLookups.FindAsync(c => c.CompanyId == companyId);
        foreach (var category in existing)
        {
            await _unitOfWork.InventoryCategoryLookups.DeleteAsync(category);
        }

        var codeIdx = Array.IndexOf(headers, "CategoryCode");
        var nameIdx = Array.IndexOf(headers, "DisplayName");
        var orderIdx = Array.IndexOf(headers, "DisplayOrder");
        var activeIdx = Array.IndexOf(headers, "IsActive");
        var systemIdx = Array.IndexOf(headers, "IsSystemDefined");
        var coatingIdx = Array.IndexOf(headers, "IsCoating");
        var descIdx = Array.IndexOf(headers, "Description");

        foreach (var row in rows)
        {
            try
            {
                var category = new Core.Entities.InventoryCategoryLookup
                {
                    CompanyId = companyId,
                    CategoryCode = row[codeIdx],
                    DisplayName = row[nameIdx],
                    DisplayOrder = int.Parse(row[orderIdx]),
                    IsActive = bool.Parse(row[activeIdx]),
                    IsSystemDefined = systemIdx >= 0 && systemIdx < row.Length ? bool.Parse(row[systemIdx]) : false,
                    IsCoating = coatingIdx >= 0 && coatingIdx < row.Length ? bool.Parse(row[coatingIdx]) : false,
                    Description = descIdx >= 0 && descIdx < row.Length ? row[descIdx] : null
                };
                await _unitOfWork.InventoryCategoryLookups.AddAsync(category);
            }
            catch (Exception ex)
            {
                errors.Add($"Inventory Category '{row[codeIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Replaces all appointment status lookup records for the company with the statuses defined
    /// in the CSV table section. All existing statuses are physically deleted before inserting
    /// the new set; callers must include every status they wish to retain.
    /// </summary>
    private async Task ImportAppointmentStatuses(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing statuses
        var existing = await _unitOfWork.AppointmentStatusLookups.FindAsync(s => s.CompanyId == companyId);
        foreach (var status in existing)
        {
            await _unitOfWork.AppointmentStatusLookups.DeleteAsync(status);
        }

        var codeIdx = Array.IndexOf(headers, "StatusCode");
        var nameIdx = Array.IndexOf(headers, "DisplayName");
        var orderIdx = Array.IndexOf(headers, "DisplayOrder");
        var colorIdx = Array.IndexOf(headers, "ColorClass");
        var iconIdx = Array.IndexOf(headers, "IconClass");
        var activeIdx = Array.IndexOf(headers, "IsActive");

        foreach (var row in rows)
        {
            try
            {
                var status = new Core.Entities.AppointmentStatusLookup
                {
                    CompanyId = companyId,
                    StatusCode = row[codeIdx],
                    DisplayName = row[nameIdx],
                    DisplayOrder = int.Parse(row[orderIdx]),
                    ColorClass = row[colorIdx],
                    IconClass = iconIdx >= 0 && iconIdx < row.Length ? row[iconIdx] : null,
                    IsActive = bool.Parse(row[activeIdx])
                };
                await _unitOfWork.AppointmentStatusLookups.AddAsync(status);
            }
            catch (Exception ex)
            {
                errors.Add($"Appointment Status '{row[codeIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Replaces all appointment type lookup records for the company with the types defined in the
    /// CSV table section. All existing types are physically deleted before inserting the new set;
    /// callers must include every type they wish to retain. The optional <c>Description</c> column
    /// defaults to null if the column is absent or the cell is empty.
    /// </summary>
    private async Task ImportAppointmentTypes(string[] headers, List<string[]> rows, int companyId, List<string> errors)
    {
        // Delete existing types
        var existing = await _unitOfWork.AppointmentTypeLookups.FindAsync(t => t.CompanyId == companyId);
        foreach (var type in existing)
        {
            await _unitOfWork.AppointmentTypeLookups.DeleteAsync(type);
        }

        var codeIdx = Array.IndexOf(headers, "TypeCode");
        var nameIdx = Array.IndexOf(headers, "DisplayName");
        var orderIdx = Array.IndexOf(headers, "DisplayOrder");
        var colorIdx = Array.IndexOf(headers, "ColorClass");
        var iconIdx = Array.IndexOf(headers, "IconClass");
        var activeIdx = Array.IndexOf(headers, "IsActive");
        var descIdx = Array.IndexOf(headers, "Description");

        foreach (var row in rows)
        {
            try
            {
                var type = new Core.Entities.AppointmentTypeLookup
                {
                    CompanyId = companyId,
                    TypeCode = row[codeIdx],
                    DisplayName = row[nameIdx],
                    DisplayOrder = int.Parse(row[orderIdx]),
                    ColorClass = row[colorIdx],
                    IconClass = iconIdx >= 0 && iconIdx < row.Length ? row[iconIdx] : null,
                    IsActive = bool.Parse(row[activeIdx]),
                    Description = descIdx >= 0 && descIdx < row.Length ? row[descIdx] : null
                };
                await _unitOfWork.AppointmentTypeLookups.AddAsync(type);
            }
            catch (Exception ex)
            {
                errors.Add($"Appointment Type '{row[codeIdx]}': {ex.Message}");
            }
        }
    }

    /// <summary>
    /// Builds a CSV string for the given invoice collection. Invoice status is written as its enum
    /// name. Monetary fields (SubTotal, TaxAmount, DiscountAmount, Total, AmountPaid, BalanceDue)
    /// are written without formatting so they can be parsed as decimals by downstream tools.
    /// </summary>
    private string GenerateInvoicesCsv(IEnumerable<Core.Entities.Invoice> invoices)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("InvoiceNumber,JobNumber,Customer,Status,InvoiceDate,DueDate,SubTotal,TaxPercent,TaxAmount,DiscountAmount,Total,AmountPaid,BalanceDue,CustomerPO,Terms,Notes");

        foreach (var invoice in invoices)
        {
            var customerName = invoice.Customer != null
                ? (!string.IsNullOrWhiteSpace(invoice.Customer.CompanyName)
                    ? invoice.Customer.CompanyName
                    : $"{invoice.Customer.ContactFirstName} {invoice.Customer.ContactLastName}".Trim())
                : "Unknown";

            sb.AppendLine($"{EscapeCsv(invoice.InvoiceNumber)},{EscapeCsv(invoice.Job?.JobNumber)}," +
                $"{EscapeCsv(customerName)},{invoice.Status}," +
                $"{invoice.InvoiceDate:yyyy-MM-dd},{invoice.DueDate?.ToString("yyyy-MM-dd")}," +
                $"{invoice.SubTotal},{invoice.TaxPercent},{invoice.TaxAmount},{invoice.DiscountAmount}," +
                $"{invoice.Total},{invoice.AmountPaid},{invoice.BalanceDue}," +
                $"{EscapeCsv(invoice.CustomerPO)},{EscapeCsv(invoice.Terms)},{EscapeCsv(invoice.Notes)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV of invoice line items — one row per item across all the given invoices. The parent
    /// invoice number and the line's revenue account number (resolved from <paramref name="accountNumberById"/>)
    /// are written so revenue attribution survives an export/import round-trip. Rows are emitted in
    /// DisplayOrder within each invoice.
    /// </summary>
    private string GenerateInvoiceItemsCsv(IEnumerable<Core.Entities.Invoice> invoices, IReadOnlyDictionary<int, string> accountNumberById)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("InvoiceNumber,Description,Quantity,UnitPrice,TotalPrice,ColorName,RevenueAccountNumber,DisplayOrder,Notes");

        foreach (var invoice in invoices)
        {
            foreach (var item in invoice.InvoiceItems.OrderBy(it => it.DisplayOrder))
            {
                var revenueAccountNumber = item.RevenueAccountId.HasValue
                    && accountNumberById.TryGetValue(item.RevenueAccountId.Value, out var num)
                        ? num : "";

                sb.AppendLine($"{EscapeCsv(invoice.InvoiceNumber)},{EscapeCsv(item.Description)}," +
                    $"{item.Quantity},{item.UnitPrice},{item.TotalPrice}," +
                    $"{EscapeCsv(item.ColorName)},{EscapeCsv(revenueAccountNumber)}," +
                    $"{item.DisplayOrder},{EscapeCsv(item.Notes)}");
            }
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given invoice payment records. The parent invoice number is
    /// resolved from the eagerly loaded <c>Invoice</c> navigation property. PaymentMethod is
    /// written as its enum name (e.g., "Cash", "BankTransferACH").
    /// </summary>
    private string GeneratePaymentsCsv(IEnumerable<Core.Entities.Payment> payments)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("InvoiceNumber,Amount,PaymentDate,PaymentMethod,DepositAccountNumber,Reference,Notes");

        foreach (var payment in payments)
        {
            sb.AppendLine($"{EscapeCsv(payment.Invoice?.InvoiceNumber)}," +
                $"{payment.Amount},{payment.PaymentDate:yyyy-MM-dd}," +
                $"{payment.PaymentMethod},{EscapeCsv(payment.DepositAccount?.AccountNumber)}," +
                $"{EscapeCsv(payment.Reference)},{EscapeCsv(payment.Notes)}");
        }

        return sb.ToString();
    }

    /// <summary>Builds the vendor bill header CSV — vendor by name, AP account by number.</summary>
    private string GenerateBillsCsv(IEnumerable<Core.Entities.Bill> bills)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("BillNumber,VendorInvoiceNumber,VendorName,APAccountNumber,BillDate,DueDate,Status,Terms,Memo,SubTotal,TaxPercent,TaxAmount,Total,AmountPaid");

        foreach (var bill in bills)
        {
            sb.AppendLine($"{EscapeCsv(bill.BillNumber)},{EscapeCsv(bill.VendorInvoiceNumber)}," +
                $"{EscapeCsv(bill.Vendor?.CompanyName)},{EscapeCsv(bill.APAccount?.AccountNumber)}," +
                $"{bill.BillDate:yyyy-MM-dd},{bill.DueDate?.ToString("yyyy-MM-dd")},{bill.Status}," +
                $"{EscapeCsv(bill.Terms)},{EscapeCsv(bill.Memo)}," +
                $"{bill.SubTotal},{bill.TaxPercent},{bill.TaxAmount},{bill.Total},{bill.AmountPaid}");
        }

        return sb.ToString();
    }

    /// <summary>Builds the bill line-item CSV — one row per line, account/job resolved by number.</summary>
    private string GenerateBillLineItemsCsv(IEnumerable<Core.Entities.Bill> bills,
        IReadOnlyDictionary<int, string> accountNumberById, IReadOnlyDictionary<int, string> jobNumberById)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("BillNumber,AccountNumber,JobNumber,Description,Quantity,UnitPrice,Amount,DisplayOrder");

        foreach (var bill in bills)
        {
            foreach (var line in bill.LineItems.OrderBy(li => li.DisplayOrder))
            {
                var accountNumber = line.AccountId.HasValue && accountNumberById.TryGetValue(line.AccountId.Value, out var an) ? an : "";
                var jobNumber = line.JobId.HasValue && jobNumberById.TryGetValue(line.JobId.Value, out var jn) ? jn : "";
                sb.AppendLine($"{EscapeCsv(bill.BillNumber)},{EscapeCsv(accountNumber)},{EscapeCsv(jobNumber)}," +
                    $"{EscapeCsv(line.Description)},{line.Quantity},{line.UnitPrice},{line.Amount},{line.DisplayOrder}");
            }
        }

        return sb.ToString();
    }

    /// <summary>Builds the customer deposit CSV — customer by name, bank account + applied invoice resolved.</summary>
    private string GenerateDepositsCsv(IEnumerable<Core.Entities.Deposit> deposits, IReadOnlyDictionary<int, string> accountNumberById)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("ReceiptNumber,CustomerName,Amount,PaymentMethod,ReceivedDate,DepositAccountNumber,AppliedToInvoiceNumber,AppliedDate,Reference,Notes");

        foreach (var deposit in deposits)
        {
            var customerName = deposit.Customer != null
                ? (!string.IsNullOrWhiteSpace(deposit.Customer.CompanyName)
                    ? deposit.Customer.CompanyName
                    : $"{deposit.Customer.ContactFirstName} {deposit.Customer.ContactLastName}".Trim())
                : "";
            var depositAccountNumber = deposit.DepositAccountId.HasValue && accountNumberById.TryGetValue(deposit.DepositAccountId.Value, out var an) ? an : "";

            sb.AppendLine($"{EscapeCsv(deposit.ReceiptNumber)},{EscapeCsv(customerName)},{deposit.Amount}," +
                $"{deposit.PaymentMethod},{deposit.ReceivedDate:yyyy-MM-dd},{EscapeCsv(depositAccountNumber)}," +
                $"{EscapeCsv(deposit.AppliedToInvoice?.InvoiceNumber)},{deposit.AppliedDate?.ToString("yyyy-MM-dd")}," +
                $"{EscapeCsv(deposit.Reference)},{EscapeCsv(deposit.Notes)}");
        }

        return sb.ToString();
    }

    /// <summary>Builds the journal entry header CSV. Lines are exported separately.</summary>
    private string GenerateJournalEntriesCsv(IEnumerable<Core.Entities.JournalEntry> entries)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("EntryNumber,EntryDate,Reference,Description,Status");

        foreach (var entry in entries)
        {
            sb.AppendLine($"{EscapeCsv(entry.EntryNumber)},{entry.EntryDate:yyyy-MM-dd}," +
                $"{EscapeCsv(entry.Reference)},{EscapeCsv(entry.Description)},{entry.Status}");
        }

        return sb.ToString();
    }

    /// <summary>Builds the journal entry line CSV — one row per debit/credit line, account by number.</summary>
    private string GenerateJournalEntryLinesCsv(IEnumerable<Core.Entities.JournalEntry> entries, IReadOnlyDictionary<int, string> accountNumberById)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("EntryNumber,AccountNumber,DebitAmount,CreditAmount,Description,LineOrder");

        foreach (var entry in entries)
        {
            foreach (var line in entry.Lines.OrderBy(l => l.LineOrder))
            {
                var accountNumber = accountNumberById.TryGetValue(line.AccountId, out var an) ? an : "";
                sb.AppendLine($"{EscapeCsv(entry.EntryNumber)},{EscapeCsv(accountNumber)}," +
                    $"{line.DebitAmount},{line.CreditAmount},{EscapeCsv(line.Description)},{line.LineOrder}");
            }
        }

        return sb.ToString();
    }

    /// <summary>
    /// Builds a CSV string for the given purchase order collection. PO status is written as its
    /// enum name. The vendor company name is resolved from the eagerly loaded <c>Vendor</c>
    /// navigation property; null-safe access means POs with a missing vendor (orphaned records)
    /// will output an empty vendor column rather than throwing.
    /// </summary>
    private string GeneratePurchaseOrdersCsv(IEnumerable<Core.Entities.PurchaseOrder> purchaseOrders)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("PoNumber,Vendor,Status,OrderDate,ExpectedDeliveryDate,ReceivedDate,SubTotal,ShippingCost,TotalAmount,Notes");

        foreach (var po in purchaseOrders)
        {
            sb.AppendLine($"{EscapeCsv(po.PoNumber)},{EscapeCsv(po.Vendor?.CompanyName)}," +
                $"{po.Status},{po.OrderDate:yyyy-MM-dd}," +
                $"{po.ExpectedDeliveryDate?.ToString("yyyy-MM-dd")},{po.ReceivedDate?.ToString("yyyy-MM-dd")}," +
                $"{po.SubTotal},{po.ShippingCost},{po.TotalAmount},{EscapeCsv(po.Notes)}");
        }

        return sb.ToString();
    }

    // GET: Tools/DownloadExpenseTemplate
    [HttpGet]
    public IActionResult DownloadExpenseTemplate()
    {
        var csvBytes = _csvImportService.GenerateExpenseTemplate();
        return File(csvBytes, "text/csv", "expense_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports expense records from a native CSV file. Resolves ExpenseAccountNumber and
    /// PaymentAccountNumber against the Chart of Accounts; VendorName and JobNumber are optional
    /// lookups. ExpenseNumber is auto-generated when blank.
    /// </summary>
    // POST: Tools/CsvImportExpenses
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportExpenses(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing expenses from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportExpensesAsync(stream, companyId.Value);
            await LogCsvImportAsync("Expenses", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                totalRows = result.TotalRows,
                skippedCount = result.SkippedCount,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing expenses from CSV");
            return Json(new { success = false, message = "An unexpected error occurred during import." });
        }
    }

    // GET: Tools/DownloadChartOfAccountsTemplate
    [HttpGet]
    public IActionResult DownloadChartOfAccountsTemplate()
    {
        var csvBytes = _csvImportService.GenerateChartOfAccountsTemplate();
        return File(csvBytes, "text/csv", "chart_of_accounts_import_template.csv");
    }

    /// <summary>
    /// Bulk-imports Chart of Accounts entries from a native CSV file. Existing accounts matched by
    /// AccountNumber are updated; new ones are created. System accounts are never modified.
    /// Returns JSON with per-row status detail.
    /// </summary>
    // POST: Tools/CsvImportChartOfAccounts
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> CsvImportChartOfAccounts(IFormFile file)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
                return Json(new { success = false, message = "Only CSV files are allowed." });

            _logger.LogInformation("User {UserName} importing chart of accounts from CSV {FileName} for company {CompanyId}",
                User.Identity?.Name, file.FileName, companyId);

            using var stream = file.OpenReadStream();
            var result = await _csvImportService.ImportChartOfAccountsAsync(stream, companyId.Value);
            await LogCsvImportAsync("ChartOfAccounts", file.FileName, result);

            return Json(new
            {
                success = result.Success,
                message = result.Summary,
                successCount = result.SuccessCount,
                skippedCount = result.SkippedCount,
                errorCount = result.ErrorCount,
                totalRows = result.TotalRows,
                errors = result.Errors,
                warnings = result.Warnings
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing chart of accounts from CSV");
            return Json(new { success = false, message = "An unexpected error occurred during import." });
        }
    }

    /// <summary>
    /// Exports all expense records for the current company as a CSV file. Account numbers are
    /// written as the GL account number (e.g. "6200") so the file can be re-imported directly.
    /// </summary>
    // GET: Tools/ExportExpensesCsv
    [HttpGet]
    public async Task<IActionResult> ExportExpensesCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var expenses = await _unitOfWork.Expenses.FindAsync(e => e.CompanyId == companyId, false,
                e => e.ExpenseAccount,
                e => e.PaymentAccount,
                e => e.Vendor,
                e => e.Job);

            var csv = GenerateExpensesCsv(expenses);
            var fileName = $"expenses_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Expenses", "CSV export");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting expenses to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting expenses.";
            return RedirectToAction(nameof(Index));
        }
    }

    private string GenerateExpensesCsv(IEnumerable<Core.Entities.Expense> expenses)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("ExpenseNumber,Date,VendorName,ExpenseAccountNumber,PaymentAccountNumber,JobNumber,PaymentMethod,Amount,Memo");

        foreach (var e in expenses.OrderBy(x => x.Date).ThenBy(x => x.ExpenseNumber))
        {
            sb.AppendLine($"{EscapeCsv(e.ExpenseNumber)},{e.Date:yyyy-MM-dd},{EscapeCsv(e.Vendor?.CompanyName)},{EscapeCsv(e.ExpenseAccount?.AccountNumber)},{EscapeCsv(e.PaymentAccount?.AccountNumber)},{EscapeCsv(e.Job?.JobNumber)},{e.PaymentMethod},{e.Amount},{EscapeCsv(e.Memo)}");
        }

        return sb.ToString();
    }

    /// <summary>
    /// Exports all prep services for the current company as a CSV file.
    /// Column names match PrepServiceImportDto exactly so the file can be re-imported.
    /// </summary>
    // GET: Tools/ExportPrepServicesCsv
    [HttpGet]
    public async Task<IActionResult> ExportPrepServicesCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var prepServices = await _unitOfWork.PrepServices.FindAsync(ps => ps.CompanyId == companyId.Value);
            var csv = GeneratePrepServicesCsv(prepServices);
            var fileName = $"prep_services_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("PrepServices", $"CSV export ({prepServices.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting prep services to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting prep services.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports all vendor records for the current company as a CSV file.
    /// Column names match <see cref="VendorImportDto"/> exactly so the file can be re-imported.
    /// </summary>
    // GET: Tools/ExportVendorsCsv
    [HttpGet]
    public async Task<IActionResult> ExportVendorsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var vendors = await _unitOfWork.Vendors.FindAsync(v => v.CompanyId == companyId.Value);
            var csv = GenerateVendorsCsv(vendors);
            var fileName = $"vendors_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("Vendors", $"CSV export ({vendors.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting vendors to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting vendors.";
            return RedirectToAction(nameof(Index));
        }
    }

    /// <summary>
    /// Exports the Chart of Accounts for the current company as a CSV file.
    /// Useful for reviewing all GL accounts and migrating the account list between environments.
    /// </summary>
    // GET: Tools/ExportChartOfAccountsCsv
    [HttpGet]
    public async Task<IActionResult> ExportChartOfAccountsCsv()
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
            {
                TempData["ErrorMessage"] = "Your account is not associated with a company.";
                return RedirectToAction(nameof(Index));
            }

            var accounts = await _unitOfWork.Accounts.FindAsync(a => a.CompanyId == companyId.Value);
            var csv = GenerateChartOfAccountsCsv(accounts);
            var fileName = $"chart_of_accounts_export_{DateTime.UtcNow:yyyyMMddHHmmss}.csv";

            await LogExportAsync("ChartOfAccounts", $"CSV export ({accounts.Count()} records)");
            return File(System.Text.Encoding.UTF8.GetBytes(csv), "text/csv", fileName);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error exporting chart of accounts to CSV");
            TempData["ErrorMessage"] = "An error occurred while exporting chart of accounts.";
            return RedirectToAction(nameof(Index));
        }
    }

    private string GenerateVendorsCsv(IEnumerable<Core.Entities.Vendor> vendors)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("CompanyName,ContactName,Email,Phone,Address,City,State,ZipCode,Country,Website,AccountNumber,TaxId,PaymentTerms,CreditLimit,IsPreferred,IsActive,Notes");

        foreach (var v in vendors)
        {
            sb.AppendLine($"{EscapeCsv(v.CompanyName)},{EscapeCsv(v.ContactName)},{EscapeCsv(v.Email)},{EscapeCsv(v.Phone)},{EscapeCsv(v.Address)},{EscapeCsv(v.City)},{EscapeCsv(v.State)},{EscapeCsv(v.ZipCode)},{EscapeCsv(v.Country)},{EscapeCsv(v.Website)},{EscapeCsv(v.AccountNumber)},{EscapeCsv(v.TaxId)},{EscapeCsv(v.PaymentTerms)},{v.CreditLimit},{v.IsPreferred.ToString().ToLower()},{v.IsActive.ToString().ToLower()},{EscapeCsv(v.Notes)}");
        }

        return sb.ToString();
    }

    private string GenerateChartOfAccountsCsv(IEnumerable<Core.Entities.Account> accounts)
    {
        var sb = new System.Text.StringBuilder();
        sb.AppendLine("AccountNumber,Name,AccountType,AccountSubType,Description,IsActive,IsSystem,OpeningBalance,OpeningBalanceDate,CurrentBalance");

        foreach (var a in accounts.OrderBy(x => x.AccountNumber))
        {
            sb.AppendLine($"{EscapeCsv(a.AccountNumber)},{EscapeCsv(a.Name)},{a.AccountType},{a.AccountSubType},{EscapeCsv(a.Description)},{a.IsActive.ToString().ToLower()},{a.IsSystem.ToString().ToLower()},{a.OpeningBalance},{a.OpeningBalanceDate?.ToString("yyyy-MM-dd")},{a.CurrentBalance}");
        }

        return sb.ToString();
    }

    private string EscapeCsv(string? value)
    {
        if (string.IsNullOrEmpty(value))
            return "";

        if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r"))
        {
            return $"\"{value.Replace("\"", "\"\"")}\"";
        }

        return value;
    }

    #endregion

    // ── QuickBooks Online Import Endpoints ────────────────────────────────────

    /// <summary>
    /// Imports customers from a QuickBooks Online CSV export file. Delegates all parsing and
    /// upsert logic to <see cref="PowderCoating.Web.Services.QuickBooksOnlineService"/> via the
    /// shared <see cref="RunQboImport"/> helper. The QBO service is injected per-action via
    /// <c>[FromServices]</c> to avoid adding it to the controller constructor, keeping the
    /// constructor lean given the number of other injected services.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQboCustomers(IFormFile file,
        [FromServices] PowderCoating.Web.Services.QuickBooksOnlineService qboService)
        => await RunQboImport(file, qboService.ImportCustomersAsync, "customers");

    /// <summary>
    /// Imports vendors from a QuickBooks Online CSV export file via <see cref="RunQboImport"/>.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQboVendors(IFormFile file,
        [FromServices] PowderCoating.Web.Services.QuickBooksOnlineService qboService)
        => await RunQboImport(file, qboService.ImportVendorsAsync, "vendors");

    /// <summary>
    /// Imports Products &amp; Services (catalog items) from a QuickBooks Online CSV export file
    /// via <see cref="RunQboImport"/>.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQboCatalogItems(IFormFile file,
        [FromServices] PowderCoating.Web.Services.QuickBooksOnlineService qboService)
        => await RunQboImport(file, qboService.ImportCatalogItemsAsync, "products & services");

    /// <summary>
    /// Imports the Chart of Accounts from a QuickBooks Online CSV export file via
    /// <see cref="RunQboImport"/>.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQboChartOfAccounts(IFormFile file,
        [FromServices] PowderCoating.Web.Services.QuickBooksOnlineService qboService)
        => await RunQboImport(file, qboService.ImportChartOfAccountsAsync, "chart of accounts");

    /// <summary>
    /// Imports historical invoices from a QuickBooks Online CSV export file via
    /// <see cref="RunQboImport"/>.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQboInvoices(IFormFile file,
        [FromServices] PowderCoating.Web.Services.QuickBooksOnlineService qboService)
        => await RunQboImport(file, qboService.ImportInvoicesAsync, "invoices");

    /// <summary>
    /// Imports general-ledger transactions from a QuickBooks Online CSV export file via
    /// <see cref="RunQboImport"/>.
    /// </summary>
    [HttpPost, ValidateAntiForgeryToken]
    public async Task<IActionResult> ImportQboTransactions(IFormFile file,
        [FromServices] PowderCoating.Web.Services.QuickBooksOnlineService qboService)
        => await RunQboImport(file, qboService.ImportTransactionsAsync, "transactions");

    /// <summary>
    /// Shared execution kernel for all QuickBooks Online CSV import actions. Validates the
    /// company context and file presence, invokes the provided <paramref name="handler"/> delegate
    /// (which is the specific QBO service method for the entity type being imported), and returns
    /// a consistent JSON response with row-level error detail. The <paramref name="label"/> string
    /// is used only for structured logging so log queries can filter by entity type without
    /// parsing the file name. All QBO import actions are thin wrappers that delegate here to avoid
    /// repeating the same validation and JSON shaping code.
    /// </summary>
    private async Task<IActionResult> RunQboImport(
        IFormFile file,
        Func<IFormFile, int, string, Task<PowderCoating.Application.DTOs.QuickBooks.ImportResultDto>> handler,
        string label)
    {
        try
        {
            var companyId = _tenantContext.GetCurrentCompanyId();
            if (companyId == null)
                return Json(new { success = false, message = "Your account is not associated with a company." });

            if (file == null || file.Length == 0)
                return Json(new { success = false, message = "No file provided or file is empty." });

            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "system";
            _logger.LogInformation("User {UserName} importing QBO {Label} from {FileName} for company {CompanyId}",
                User.Identity?.Name, label, file.FileName, companyId);

            var result = await handler(file, companyId.Value, userId);
            await LogImportAsync(label, file.FileName, result);

            return Json(new
            {
                success       = result.Success,
                message       = result.Success ? $"Import completed successfully!" : "Import completed with errors.",
                totalRecords  = result.TotalRecords,
                importedCount = result.ImportedCount,
                updatedCount  = result.UpdatedCount,
                skippedCount  = result.SkippedCount,
                errors        = result.Errors.Select(e => new
                {
                    severity       = e.Severity,
                    lineNumber     = e.LineNumber,
                    recordName     = e.RecordName,
                    fieldName      = e.FieldName,
                    errorMessage   = e.ErrorMessage,
                    displayMessage = e.DisplayMessage
                }).ToList()
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error importing QBO {Label}", label);
            return Json(new { success = false, message = $"An error occurred during import: {ex.Message}" });
        }
    }
}
