using Extrudex.Domain.DTOs.Moonraker;
using Extrudex.Domain.Entities;
using Extrudex.Domain.Enums;
using Extrudex.Domain.Interfaces;
using Extrudex.Infrastructure.Configuration;
using Extrudex.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Extrudex.Infrastructure.Services;

/// <summary>
/// Service that syncs Moonraker printer status and print job history into the
/// Extrudex database. Queries all active Moonraker printers, fetches their
/// current operational state, and maps completed print jobs to PrintJob and
/// FilamentUsage entities with derived gram calculations.
/// </summary>
public class MoonrakerPrinterSyncService : IMoonrakerPrinterSyncService
{
    private readonly ExtrudexDbContext _dbContext;
    private readonly IMoonrakerClient _moonrakerClient;
    private readonly ILogger<MoonrakerPrinterSyncService> _logger;

    /// <summary>
    /// Creates a new MoonrakerPrinterSyncService.
    /// </summary>
    /// <param name="dbContext">The EF Core database context for persisting updates.</param>
    /// <param name="moonrakerClient">The Moonraker HTTP client for fetching printer data.</param>
    /// <param name="logger">Logger for diagnostic output.</param>
    public MoonrakerPrinterSyncService(
        ExtrudexDbContext dbContext,
        IMoonrakerClient moonrakerClient,
        ILogger<MoonrakerPrinterSyncService> logger)
    {
        _dbContext = dbContext;
        _moonrakerClient = moonrakerClient;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<int> SyncAllAsync(CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Starting Moonraker printer sync cycle");

        var printers = await _dbContext.Printers
            .Where(p => p.IsActive && p.ConnectionType == ConnectionType.Moonraker)
            .Include(p => p.AmsUnits)
                .ThenInclude(u => u.Slots)
                    .ThenInclude(s => s.Spool)
                        .ThenInclude(s => s.MaterialBase)
            .Include(p => p.PrintJobs)
            .ToListAsync(cancellationToken);

        if (printers.Count == 0)
        {
            _logger.LogInformation("No active Moonraker printers found — skipping sync");
            return 0;
        }

        _logger.LogInformation("Found {PrinterCount} active Moonraker printer(s) to sync", printers.Count);

        var syncedCount = 0;

        foreach (var printer in printers)
        {
            try
            {
                await SyncPrinterAsync(printer, cancellationToken);
                syncedCount++;
            }
            catch (Exception ex) when (ex is not OperationCanceledException)
            {
                _logger.LogError(ex,
                    "Error syncing printer {PrinterName} ({Host}:{Port})",
                    printer.Name, printer.HostnameOrIp, printer.Port);

                // Mark printer as offline if we can't reach it
                printer.Status = PrinterStatus.Offline;
            }
        }

        await _dbContext.SaveChangesAsync(cancellationToken);

        _logger.LogInformation(
            "Moonraker printer sync cycle complete — {SyncedCount}/{TotalCount} printers synced",
            syncedCount, printers.Count);

        return syncedCount;
    }

    /// <summary>
    /// Syncs a single Moonraker printer: updates its status, fetches print history,
    /// and maps new print jobs to database entities.
    /// </summary>
    private async Task SyncPrinterAsync(Printer printer, CancellationToken cancellationToken)
    {
        // Step 1: Fetch printer status
        var printerInfo = await _moonrakerClient.GetPrinterInfoAsync(
            printer.HostnameOrIp, printer.Port, printer.ApiKey, cancellationToken);

        var printStats = await _moonrakerClient.GetPrintStatsAsync(
            printer.HostnameOrIp, printer.Port, printer.ApiKey, cancellationToken);

        // Step 2: Update printer status
        UpdatePrinterStatus(printer, printerInfo, printStats);
        printer.LastSeenAt = DateTime.UtcNow;

        _logger.LogDebug(
            "Printer {PrinterName} status updated to {Status}",
            printer.Name, printer.Status);

        // Step 3: Fetch and map print job history
        var history = await _moonrakerClient.GetPrintHistoryAsync(
            printer.HostnameOrIp, printer.Port, printer.ApiKey,
            limit: 25,
            cancellationToken);

        if (history.Items.Count == 0)
        {
            _logger.LogDebug("No print history returned for printer {PrinterName}", printer.Name);
            return;
        }

        var newJobsCount = await MapPrintJobsAsync(printer, history.Items, cancellationToken);

        if (newJobsCount > 0)
        {
            _logger.LogInformation(
                "Mapped {NewJobsCount} new print job(s) from printer {PrinterName}",
                newJobsCount, printer.Name);
        }
    }

    /// <summary>
    /// Updates the printer's operational status based on Moonraker telemetry.
    /// Maps Klipper/Moonraker state strings to the PrinterStatus enum.
    /// </summary>
    private void UpdatePrinterStatus(
        Printer printer,
        MoonrakerPrinterInfo? printerInfo,
        MoonrakerPrintStats? printStats)
    {
        // Prefer print_stats state — it's the most authoritative
        if (printStats != null)
        {
            printer.Status = printStats.State.ToLowerInvariant() switch
            {
                "printing" => PrinterStatus.Printing,
                "paused" => PrinterStatus.Paused,
                "complete" => PrinterStatus.Idle,
                "standby" => PrinterStatus.Idle,
                "cancelled" => PrinterStatus.Idle,
                "error" => PrinterStatus.Error,
                _ => PrinterStatus.Idle
            };
            return;
        }

        // Fall back to printer_info state
        if (printerInfo != null)
        {
            printer.Status = printerInfo.State.ToLowerInvariant() switch
            {
                "ready" => PrinterStatus.Idle,
                "startup" => PrinterStatus.Idle,
                "shutdown" => PrinterStatus.Offline,
                "error" => PrinterStatus.Error,
                "cancelled" => PrinterStatus.Idle,
                _ => printer.Status // Preserve existing status if unknown
            };
        }
    }

    /// <summary>
    /// Maps Moonraker print job history items to Extrudex PrintJob and FilamentUsage entities.
    /// Only creates records for jobs not already tracked (by Moonraker JobId stored in GcodeFilePath).
    /// </summary>
    private async Task<int> MapPrintJobsAsync(
        Printer printer,
        List<MoonrakerPrintJob> historyItems,
        CancellationToken cancellationToken)
    {
        // Build a set of already-tracked Moonraker JobIds for this printer
        // We store the Moonraker JobId in the GcodeFilePath field with a "moonraker:" prefix
        var trackedJobIds = await _dbContext.PrintJobs
            .Where(pj => pj.PrinterId == printer.Id && pj.GcodeFilePath != null && pj.GcodeFilePath.StartsWith("moonraker:"))
            .Select(pj => pj.GcodeFilePath!)
            .ToListAsync(cancellationToken);

        var trackedIdSet = new HashSet<string>(trackedJobIds);
        var newJobsCount = 0;

        // Find the default spool for this printer (first active spool in AMS, or first active spool overall)
        var defaultSpool = FindDefaultSpool(printer);

        foreach (var moonrakerJob in historyItems)
        {
            var jobIdKey = $"moonraker:{moonrakerJob.JobId}";

            if (trackedIdSet.Contains(jobIdKey))
            {
                continue; // Already tracked — skip
            }

            // Only map completed, cancelled, or errored jobs (not in_progress)
            // In-progress jobs will be captured on the next cycle once they finish
            if (moonrakerJob.Status == "in_progress")
            {
                continue;
            }

            // Map Moonraker job status to JobStatus enum
            var jobStatus = moonrakerJob.Status.ToLowerInvariant() switch
            {
                "completed" => JobStatus.Completed,
                "cancelled" => JobStatus.Cancelled,
                "error" => JobStatus.Failed,
                _ => JobStatus.Completed
            };

            // Calculate derived grams if we have a spool and filament data
            decimal gramsDerived = 0m;
            decimal filamentDiameterMm = 1.75m;
            decimal materialDensity = 1.24m; // PLA default

            if (defaultSpool != null)
            {
                filamentDiameterMm = defaultSpool.FilamentDiameterMm;
                materialDensity = defaultSpool.MaterialBase.DensityGperCm3;
                gramsDerived = CalculateGrams(moonrakerJob.FilamentUsedMm, filamentDiameterMm, materialDensity);
            }
            else if (moonrakerJob.FilamentUsedMm > 0)
            {
                gramsDerived = CalculateGrams(moonrakerJob.FilamentUsedMm, 1.75m, 1.24m);
                _logger.LogWarning(
                    "No default spool found for printer {PrinterName} — using PLA defaults for grams derivation on job {JobId}",
                    printer.Name, moonrakerJob.JobId);
            }

            var printJob = new PrintJob
            {
                PrinterId = printer.Id,
                SpoolId = defaultSpool?.Id ?? Guid.Empty,
                PrintName = moonrakerJob.Filename,
                GcodeFilePath = jobIdKey,
                MmExtruded = moonrakerJob.FilamentUsedMm,
                GramsDerived = gramsDerived,
                StartedAt = moonrakerJob.StartTime,
                CompletedAt = moonrakerJob.EndTime,
                Status = jobStatus,
                DataSource = DataSource.Moonraker,
                FilamentDiameterAtPrintMm = filamentDiameterMm,
                MaterialDensityAtPrint = materialDensity,
                Notes = $"Auto-imported from Moonraker (JobId: {moonrakerJob.JobId})"
            };

            _dbContext.PrintJobs.Add(printJob);

            // Create a FilamentUsage record if filament was consumed
            if (moonrakerJob.FilamentUsedMm > 0 && defaultSpool != null)
            {
                var usage = new FilamentUsage
                {
                    PrintJob = printJob,
                    SpoolId = defaultSpool.Id,
                    PrinterId = printer.Id,
                    GramsUsed = gramsDerived,
                    MmExtruded = moonrakerJob.FilamentUsedMm,
                    RecordedAt = DateTime.UtcNow,
                    Notes = $"Auto-imported from Moonraker history (JobId: {moonrakerJob.JobId})"
                };

                _dbContext.FilamentUsages.Add(usage);
            }

            newJobsCount++;
            trackedIdSet.Add(jobIdKey); // Prevent duplicates within this batch
        }

        return newJobsCount;
    }

    /// <summary>
    /// Finds the default spool for a printer. Returns the first spool loaded
    /// in an AMS slot, or null if no spool is available.
    /// </summary>
    private static Spool? FindDefaultSpool(Printer printer)
    {
        // Prefer the first active spool in an AMS slot
        foreach (var amsUnit in printer.AmsUnits)
        {
            foreach (var slot in amsUnit.Slots)
            {
                if (slot.Spool != null && slot.Spool.IsActive && !slot.Spool.IsArchived)
                {
                    return slot.Spool;
                }
            }
        }

        return null;
    }

    /// <summary>
    /// Calculates derived grams from millimeters extruded using the standard formula:
    /// grams = mm_extruded × cross_section_area × material_density
    /// where cross_section_area = π × (diameter / 2)²
    /// </summary>
    private static decimal CalculateGrams(decimal mmExtruded, decimal diameterMm, decimal densityGperCm3)
    {
        if (mmExtruded <= 0) return 0m;

        var radiusCm = (double)diameterMm / 2.0 / 10.0; // mm to cm
        var crossSectionAreaCm2 = Math.PI * radiusCm * radiusCm;
        var mmToCm = (double)mmExtruded / 10.0;

        var grams = mmToCm * crossSectionAreaCm2 * (double)densityGperCm3;
        return (decimal)grams;
    }
}