using System.Runtime.InteropServices;
using Sbroenne.ExcelMcp.ComInterop;
using Sbroenne.ExcelMcp.ComInterop.Session;

namespace Sbroenne.ExcelMcp.Core.Commands.Screenshot;

/// <summary>
/// Implementation of screenshot commands using Excel COM CopyPicture + ChartObject.Export.
/// </summary>
public class ScreenshotCommands : IScreenshotCommands
{
    // Excel COM constants
    private const int XlScreen = 1;      // xlScreen - required for CopyPicture to render correctly
    private const int XlPrinter = 2;     // xlPrinter - fallback when xlScreen rendering is unavailable
    private const int XlPicture = -4147; // xlPicture - more reliable than xlBitmap for range capture
    private const int XlBitmap = 2;      // xlBitmap - fallback when xlPicture fails
    private const int XlNormal = -4143;  // xlNormal
    private const int XlMinimized = -4140; // xlMinimized
    private const int SwRestore = 9;

    // CopyPicture retry configuration
    // After Save or large operations, Excel rendering can take several seconds.
    // Retries with exponential backoff: 500ms, 1000ms, 1500ms, 2000ms, 2500ms, 3000ms, 3500ms
    private const int CopyPictureMaxRetries = 10;
    private const int CopyPictureRetryDelayMs = 700;

    // Export format constants
    private const string FormatPng = "PNG";
    private const string FormatJpeg = "JPEG";

    /// <summary>
    /// Captures a specific range as an image.
    /// </summary>
    public ScreenshotResult CaptureRange(IExcelBatch batch, string? sheetName = null, string rangeAddress = "A1:Z30", ScreenshotQuality quality = ScreenshotQuality.Medium)
    {
        return batch.Execute((ctx, ct) =>
        {
            dynamic? sheet = null;
            dynamic? range = null;
            try
            {
                sheet = string.IsNullOrWhiteSpace(sheetName)
                    ? ctx.Book.ActiveSheet
                    : ctx.Book.Worksheets[sheetName];

                range = sheet.Range[rangeAddress];
                string actualSheet = sheet.Name?.ToString() ?? "Sheet1";
                string actualRange = range.Address?.ToString() ?? rangeAddress;

                return ExportRangeAsImage(ctx.App, sheet, range, actualSheet, actualRange, quality);
            }
            finally
            {
                ComUtilities.Release(ref range);
                ComUtilities.Release(ref sheet);
            }
        });
    }

    /// <summary>
    /// Captures the entire used area of a worksheet as an image.
    /// If UsedRange exceeds 500 rows or 50 columns, it is capped to avoid
    /// CopyPicture failures on sheets with formatting extending far beyond data.
    /// </summary>
    public ScreenshotResult CaptureSheet(IExcelBatch batch, string? sheetName = null, ScreenshotQuality quality = ScreenshotQuality.Medium)
    {
        return batch.Execute((ctx, ct) =>
        {
            dynamic? sheet = null;
            dynamic? usedRange = null;
            dynamic? captureRange = null;
            try
            {
                sheet = string.IsNullOrWhiteSpace(sheetName)
                    ? ctx.Book.ActiveSheet
                    : ctx.Book.Worksheets[sheetName];

                usedRange = sheet.UsedRange;
                string actualSheet = sheet.Name?.ToString() ?? "Sheet1";

                int rows = (int)usedRange.Rows.Count;
                int cols = (int)usedRange.Columns.Count;

                const int maxRows = 500;
                const int maxCols = 50;

                if (rows > maxRows || cols > maxCols)
                {
                    // Cap the range to avoid CopyPicture failures on enormous ranges
                    int startRow = (int)usedRange.Row;
                    int startCol = (int)usedRange.Column;
                    int endRow = startRow + Math.Min(rows, maxRows) - 1;
                    int endCol = startCol + Math.Min(cols, maxCols) - 1;
                    captureRange = sheet.Range[sheet.Cells[startRow, startCol], sheet.Cells[endRow, endCol]];
                }

                dynamic rangeToCapture = captureRange ?? usedRange;
                string actualRange = rangeToCapture.Address?.ToString() ?? "A1";

                return ExportRangeAsImage(ctx.App, sheet, rangeToCapture, actualSheet, actualRange, quality);
            }
            finally
            {
                ComUtilities.Release(ref captureRange);
                ComUtilities.Release(ref usedRange);
                ComUtilities.Release(ref sheet);
            }
        });
    }

    /// <summary>
    /// Exports a range as a PNG image using CopyPicture + ChartObject.Export.
    /// CopyPicture requires Excel to be visible for rendering. If Excel is hidden,
    /// we temporarily show it, capture, then restore the previous visibility state.
    ///
    /// CRITICAL: After Save/large operations, Excel needs rendering time even if already visible.
    /// This method includes delays to ensure Excel is fully rendered before capture.
    /// </summary>
    private static ScreenshotResult ExportRangeAsImage(dynamic app, dynamic sheet, dynamic range, string sheetName, string rangeAddress, ScreenshotQuality quality)
    {
        dynamic? chartObjects = null;
        dynamic? chartObject = null;
        dynamic? chart = null;
        string? tempFile = null;
        bool wasVisible = false;
        int originalWindowState = XlNormal;
        bool windowStateChanged = false;

        try
        {
            // CopyPicture requires Excel to be visible for UI rendering
            wasVisible = (bool)app.Visible;
            if (!wasVisible)
            {
                app.Visible = true;

                // Excel needs time to initialize its rendering pipeline after
                // becoming visible. Without this, CopyPicture fails with
                // "Unable to get the CopyPicture property" or crashes the process
                // with RPC_S_SERVER_UNAVAILABLE (0x800706BA) under rapid cycling.
                Thread.Sleep(2000);
            }
            else
            {
                // Excel is already visible, but may still be rendering from previous operations.
                // Slightly longer delay to allow rendering pipeline to fully settle.
                Thread.Sleep(1000);
            }

            originalWindowState = Convert.ToInt32(app.WindowState);
            if (originalWindowState == XlMinimized)
            {
                app.WindowState = XlNormal;
                windowStateChanged = true;
                Thread.Sleep(500);
            }

            BringExcelToForeground(app);
            PrepareRangeForCapture(app, sheet, range);

            // Get range dimensions for the chart
            double width = Convert.ToDouble(range.Width);
            double height = Convert.ToDouble(range.Height);

            // Cap dimensions to avoid huge images (max ~4096px equivalent at 96 DPI)
            // Excel Width/Height are in points (1 point = 1.333 pixels at 96 DPI)
            const double maxPoints = 3072; // ~4096px
            if (width > maxPoints || height > maxPoints)
            {
                double scale = Math.Min(maxPoints / width, maxPoints / height);
                width *= scale;
                height *= scale;
            }

            // Apply quality-based scale reduction before export.
            // Lower quality = smaller chart dimensions = fewer pixels = smaller image.
            // JPEG format (Medium/Low) is also ~4-8x smaller than PNG for the same content.
            double qualityScale = quality switch
            {
                ScreenshotQuality.Low => 0.5,
                ScreenshotQuality.Medium => 0.75,
                _ => 1.0  // High: full scale
            };
            width *= qualityScale;
            height *= qualityScale;

            // Minimum useful size
            width = Math.Max(width, 50);
            height = Math.Max(height, 30);

            // Export format: JPEG for Medium/Low (much smaller), PNG for High (lossless)
            string exportFormat = quality == ScreenshotQuality.High ? FormatPng : FormatJpeg;
            string mimeType = quality == ScreenshotQuality.High ? "image/png" : "image/jpeg";
            string fileExt = quality == ScreenshotQuality.High ? "png" : "jpg";

            // Copy range as picture (with retry — CopyPicture is clipboard-dependent
            // and intermittently fails when Excel is still rendering after chart/table operations)
            try { app.CutCopyMode = false; } catch (COMException) { }
            CopyPictureWithRetry(app, sheet, range);
            Thread.Sleep(250);

            // Create a temporary ChartObject to paste into and export
            chartObjects = sheet.ChartObjects();
            chartObject = chartObjects.Add(0, 0, width, height);
            chart = chartObject.Chart;

            // Paste the copied picture into the chart
            PastePictureWithRetry(app, sheet, range, chart);

            // Clear clipboard immediately after paste — releases clipboard for subsequent screenshot calls
            // (otherwise marching ants remain and next CopyPicture may fail with clipboard contention)
            try { app.CutCopyMode = false; } catch (COMException) { }

            // Export to temp file in the appropriate format
            tempFile = Path.Combine(Path.GetTempPath(), $"excelmcp-screenshot-{Guid.NewGuid():N}.{fileExt}");
            chart.Export(tempFile, exportFormat);

            // Read and convert to base64
            byte[] imageBytes = File.ReadAllBytes(tempFile);
            string base64 = Convert.ToBase64String(imageBytes);

            // Read pixel dimensions from file header
            (int pixelWidth, int pixelHeight) = quality == ScreenshotQuality.High
                ? GetPngDimensions(imageBytes)
                : GetJpegDimensions(imageBytes);

            return new ScreenshotResult
            {
                Success = true,
                ImageBase64 = base64,
                MimeType = mimeType,
                Width = pixelWidth,
                Height = pixelHeight,
                SheetName = sheetName,
                RangeAddress = rangeAddress,
                Message = $"Captured {rangeAddress} on '{sheetName}' ({pixelWidth}x{pixelHeight}px)"
            };
        }
        finally
        {
            // Clean up temp ChartObject from the worksheet
            if (chartObject != null)
            {
                try { chartObject.Delete(); } catch { /* best effort */ }
            }

            ComUtilities.Release(ref chart);
            ComUtilities.Release(ref chartObject);
            ComUtilities.Release(ref chartObjects);

            // Restore Excel visibility if we changed it
            if (!wasVisible)
            {
                try { app.Visible = false; } catch (COMException) { }
            }

            if (windowStateChanged)
            {
                try { app.WindowState = originalWindowState; } catch (COMException) { }
            }

            // Clean up temp file
            if (tempFile != null && File.Exists(tempFile))
            {
                try { File.Delete(tempFile); } catch { /* best effort */ }
            }
        }
    }

    /// <summary>
    /// Calls CopyPicture with retry logic. CopyPicture uses the clipboard and
    /// intermittently fails with COMException when Excel is busy rendering
    /// (e.g., after chart/table operations). Retries with increasing delay.
    /// </summary>
    private static void CopyPictureWithRetry(dynamic app, dynamic sheet, dynamic range)
    {
        COMException? lastException = null;
        (int Appearance, int Format)[] modes =
        [
            (XlScreen, XlPicture),
            (XlPrinter, XlPicture),
            (XlScreen, XlBitmap),
            (XlPrinter, XlBitmap)
        ];

        for (int attempt = 0; attempt < CopyPictureMaxRetries; attempt++)
        {
            foreach (var mode in modes)
            {
                try
                {
                    try { app.CutCopyMode = false; } catch (COMException) { }
                    range.CopyPicture(mode.Appearance, mode.Format);
                    return;
                }
                catch (COMException ex)
                {
                    lastException = ex;
                }
            }

            if (attempt < CopyPictureMaxRetries - 1)
            {
                Thread.Sleep(CopyPictureRetryDelayMs * (attempt + 1));
                PrepareRangeForCapture(app, sheet, range);
            }
        }

        for (int attempt = 0; attempt < CopyPictureMaxRetries; attempt++)
        {
            try
            {
                try { app.CutCopyMode = false; } catch (COMException) { }
                range.Copy();
                return;
            }
            catch (COMException ex)
            {
                lastException = ex;
                if (attempt < CopyPictureMaxRetries - 1)
                {
                    Thread.Sleep(CopyPictureRetryDelayMs * (attempt + 1));
                    PrepareRangeForCapture(app, sheet, range);
                }
            }
        }

        throw new InvalidOperationException(
            $"Excel could not capture the range after {CopyPictureMaxRetries} attempts because CopyPicture and the range copy fallback kept failing. " +
            "Excel may still be rendering, busy, minimized, or unable to access the clipboard. " +
            "Retry the screenshot after the workbook finishes refreshing, or capture a smaller range.",
            lastException);
    }

    private static void PrepareRangeForCapture(dynamic app, dynamic sheet, dynamic range)
    {
        dynamic? topLeftCell = null;
        dynamic? activeWindow = null;

        try
        {
            BringExcelToForeground(app);
            try { sheet.Activate(); } catch (COMException) { }

            try
            {
                topLeftCell = range.Cells[1, 1];
                app.Goto(topLeftCell, true);
            }
            catch (COMException) { }

            try
            {
                activeWindow = app.ActiveWindow;
                if (activeWindow != null && topLeftCell != null)
                {
                    activeWindow.ScrollRow = Convert.ToInt32(topLeftCell.Row);
                    activeWindow.ScrollColumn = Convert.ToInt32(topLeftCell.Column);
                }
            }
            catch (COMException) { }

            try { range.Select(); } catch (COMException) { }
            Thread.Sleep(500);
        }
        finally
        {
            ComUtilities.Release(ref activeWindow);
            ComUtilities.Release(ref topLeftCell);
        }
    }

    private static void BringExcelToForeground(dynamic app)
    {
        try
        {
            IntPtr hwnd = new(Convert.ToInt64(app.Hwnd));
            if (hwnd != IntPtr.Zero)
            {
                ShowWindow(hwnd, SwRestore);
                SetForegroundWindow(hwnd);
                Thread.Sleep(250);
            }
        }
        catch (COMException) { }
        catch (InvalidCastException) { }
        catch (FormatException) { }
        catch (OverflowException) { }
    }

    [DllImport("user32.dll")]
    private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    private static void PastePictureWithRetry(dynamic app, dynamic sheet, dynamic range, dynamic chart)
    {
        for (int attempt = 0; attempt < CopyPictureMaxRetries; attempt++)
        {
            try
            {
                try { chart.Parent.Activate(); } catch (COMException) { }
                try { chart.ChartArea.Select(); } catch (COMException) { }
                Thread.Sleep(100);
                chart.Paste();
                Thread.Sleep(250);
                return;
            }
            catch (COMException) when (attempt < CopyPictureMaxRetries - 1)
            {
                Thread.Sleep(CopyPictureRetryDelayMs * (attempt + 1));
                PrepareRangeForCapture(app, sheet, range);
                try { app.CutCopyMode = false; } catch (COMException) { }
                CopyPictureWithRetry(app, sheet, range);
            }
        }
    }

    /// <summary>
    /// Reads width and height from PNG file header (IHDR chunk).
    /// PNG format: 8-byte signature, then IHDR chunk with width (4 bytes) and height (4 bytes).
    /// </summary>
    private static (int width, int height) GetPngDimensions(byte[] data)
    {
        if (data.Length < 24)
            return (0, 0);

        // PNG IHDR starts at byte 16 (after 8-byte signature + 4-byte length + 4-byte "IHDR")
        int width = (data[16] << 24) | (data[17] << 16) | (data[18] << 8) | data[19];
        int height = (data[20] << 24) | (data[21] << 16) | (data[22] << 8) | data[23];

        return (width, height);
    }

    /// <summary>
    /// Reads width and height from JPEG file by scanning for SOF (Start Of Frame) markers.
    /// SOF markers (0xFF 0xC0..0xC3, 0xC5..0xC7, 0xC9..0xCB, 0xCD..0xCF) contain dimensions.
    /// </summary>
    private static (int width, int height) GetJpegDimensions(byte[] data)
    {
        if (data.Length < 4 || data[0] != 0xFF || data[1] != 0xD8)
            return (0, 0);

        int i = 2;
        while (i < data.Length - 8)
        {
            if (data[i] != 0xFF) break;
            byte marker = data[i + 1];

            // SOF markers that contain frame dimensions
            bool isSof = marker is >= 0xC0 and <= 0xC3
                or >= 0xC5 and <= 0xC7
                or >= 0xC9 and <= 0xCB
                or >= 0xCD and <= 0xCF;

            int segmentLength = (data[i + 2] << 8) | data[i + 3];

            if (isSof && i + 8 < data.Length)
            {
                int height = (data[i + 5] << 8) | data[i + 6];
                int width = (data[i + 7] << 8) | data[i + 8];
                return (width, height);
            }

            i += 2 + segmentLength;
        }

        return (0, 0);
    }
}
