using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Polly;
using Excel = Microsoft.Office.Interop.Excel;

namespace Sbroenne.ExcelMcp.ComInterop.Session;

/// <summary>
/// Centralized service for Excel workbook close and application quit operations.
/// Implements resilient shutdown with exponential backoff for COM busy conditions.
/// </summary>
public static class ExcelShutdownService
{
    private static readonly ResiliencePipeline _quitPipeline = ResiliencePipelines.CreateExcelQuitPipeline();

    /// <summary>
    /// Saves an Excel workbook on the calling STA thread.
    /// Must be called from within <c>ExcelBatch.Execute()</c> so the Save() COM call
    /// runs on the correct STA thread. Timeout protection is provided by the surrounding
    /// <c>ExcelBatch.Execute()</c> operation timeout and the Dispose() force-kill chain.
    /// </summary>
    /// <param name="workbook">Excel workbook COM object to save</param>
    /// <param name="fileName">File name for diagnostic messages (optional)</param>
    /// <param name="logger">Logger for diagnostic output (optional)</param>
    /// <param name="cancellationToken">Cancellation token checked before Save() is invoked</param>
    /// <exception cref="OperationCanceledException">Cancellation was requested before save started</exception>
    /// <exception cref="COMException">Save failed due to COM error</exception>
    /// <exception cref="InvalidOperationException">Save failed due to unexpected error</exception>
    public static void SaveWorkbookWithTimeout(
        Excel.Workbook workbook,
        string? fileName = null,
        ILogger? logger = null,
        CancellationToken cancellationToken = default)
    {
        logger ??= NullLogger.Instance;
        fileName ??= "unknown";

        // Honour any cancellation request before we start the potentially slow COM call
        cancellationToken.ThrowIfCancellationRequested();

        logger.LogDebug("Saving workbook {FileName}", fileName);

        try
        {
            // Call Save() directly on the STA thread.
            // Previously this used Task.Run(() => workbook.Save()) which marshalled the COM call
            // from the STA thread to an MTA thread-pool thread — crossing COM apartment boundaries
            // and risking RPC_E_WRONG_THREAD or deadlocks (GitHub #482, Bug 1).
            // This method is always called from inside ExcelBatch.Execute() which already runs
            // on the dedicated STA thread, so a direct call is correct and safe.
            const int maxSaveAttempts = 3;
            const int saveRetryDelayMs = 500;

            for (int attempt = 1; attempt <= maxSaveAttempts; attempt++)
            {
                try
                {
                    workbook.Save();
                    logger.LogDebug("Workbook {FileName} saved successfully", fileName);
                    return; // Success — exit method
                }
                catch (COMException ex) when (
                    attempt < maxSaveAttempts &&
                    (ex.HResult == unchecked((int)0x800A03EC) || ex.HResult == unchecked((int)0x800AC472)))
                {
                    // File locked by another process or antivirus — retry with delay
                    logger.LogDebug("Save attempt {Attempt} for {FileName} got transient lock (0x{HResult:X8}), retrying in {Delay}ms",
                        attempt, fileName, ex.HResult, saveRetryDelayMs * attempt);
                    Thread.Sleep(saveRetryDelayMs * attempt);
                }
                // Other COMException falls through to existing catch block below
            }
        }
        catch (COMException ex)
        {
            string errorMessage = ex.HResult switch
            {
                unchecked((int)0x800A03EC) =>
                    $"Cannot save '{fileName}'. " +
                    "The file may be read-only, locked by another process, or the path may not exist.",
                unchecked((int)0x800AC472) =>
                    $"Cannot save '{fileName}'. " +
                    "The file is locked for editing by another user or process.",
                ResiliencePipelines.RPC_E_DISCONNECTED =>
                    $"Cannot save '{fileName}'. Excel disconnected from automation before the save completed. " +
                    "The session is no longer usable; reopen the workbook and verify whether changes were saved.",
                _ => $"Failed to save workbook '{fileName}': {ex.Message}"
            };

            logger.LogError(ex, "Save failed for {FileName} (HResult: 0x{HResult:X8})", fileName, ex.HResult);
            throw new InvalidOperationException(errorMessage, ex);
        }
        // All other exceptions propagate; no generic catch block.
    }

    /// <summary>
    /// Closes a workbook and quits the Excel application with resilient retry logic.
    /// Handles save semantics, workbook close, COM object release, and resilient Quit with backoff.
    /// </summary>
    /// <param name="workbook">Excel workbook COM object (can be null)</param>
    /// <param name="excel">Excel application COM object (can be null)</param>
    /// <param name="save">True to save before closing, false to discard changes</param>
    /// <param name="filePath">File path for diagnostic logging (optional)</param>
    /// <param name="logger">Logger for diagnostic output (optional)</param>
    /// <remarks>
    /// <para><b>Shutdown Order:</b></para>
    /// <list type="number">
    /// <item>If save=true: Call workbook.Save()</item>
    /// <item>Close workbook with Close(save) - save param controls Excel's save prompt</item>
    /// <item>Release workbook COM reference</item>
    /// <item>Quit Excel application with exponential backoff retry (6 attempts, 200ms base delay)</item>
    /// <item>Release Excel COM reference</item>
    /// <item>Force GC collection to release final COM proxies</item>
    /// </list>
    /// <para><b>Resilience:</b> Retries Quit() on COM busy errors (RPC_E_SERVERCALL_RETRYLATER, RPC_E_CALL_REJECTED)</para>
    /// <para><b>Timeout:</b> No overall timeout - relies on retry exhaustion. Non-retriable errors bubble immediately.</para>
    /// </remarks>
    public static void CloseAndQuit(
        Excel.Workbook? workbook,
        Excel.Application? excel,
        bool save,
        string? filePath = null,
        ILogger? logger = null)
    {
        logger ??= NullLogger.Instance;
        string fileName = string.IsNullOrEmpty(filePath) ? "unknown" : Path.GetFileName(filePath);

        var stopwatch = Stopwatch.StartNew();

        try
        {
            // Step 1: Explicit save if requested (before Close call)
            if (save && workbook != null)
            {
                SaveWorkbookWithTimeout(workbook, fileName, logger);
            }

            // Step 2: Close workbook with retry for transient COM busy errors
            if (workbook != null)
            {
                const int maxCloseAttempts = 3;
                const int closeRetryDelayMs = 200;

                for (int attempt = 1; attempt <= maxCloseAttempts; attempt++)
                {
                    try
                    {
                        logger.LogDebug("Closing workbook {FileName} (save={Save}, attempt {Attempt})", fileName, save, attempt);
                        workbook.Close(save);
                        logger.LogDebug("Workbook {FileName} closed successfully", fileName);
                        break; // Success
                    }
                    catch (COMException ex) when (
                        attempt < maxCloseAttempts &&
                        (ex.HResult == ResiliencePipelines.RPC_E_SERVERCALL_RETRYLATER ||
                         ex.HResult == ResiliencePipelines.RPC_E_CALL_REJECTED))
                    {
                        logger.LogDebug("Workbook close attempt {Attempt} got transient error (0x{HResult:X8}), retrying in {Delay}ms",
                            attempt, ex.HResult, closeRetryDelayMs);
                        Thread.Sleep(closeRetryDelayMs * attempt); // Simple linear backoff
                    }
                    catch (COMException ex)
                    {
                        if (ex.HResult == ResiliencePipelines.RPC_E_DISCONNECTED)
                        {
                            logger.LogWarning(ex,
                                "Workbook COM proxy disconnected while closing {FileName} (HResult: 0x{HResult:X8}) - continuing with cleanup",
                                fileName, ex.HResult);
                        }
                        else
                        {
                            logger.LogWarning(ex,
                                "Failed to close workbook {FileName} (HResult: 0x{HResult:X8}) - continuing with cleanup",
                                fileName, ex.HResult);
                        }

                        break; // Non-transient error, move on
                    }
                    catch (MissingMemberException ex)
                    {
                        logger.LogWarning(ex,
                            "Workbook COM proxy was disconnected while calling Close for {FileName} - continuing with cleanup",
                            fileName);
                        break; // COM proxy dead, move on
                    }
                }

                // Release workbook COM reference (moved out of individual catch blocks)
                try { Marshal.ReleaseComObject(workbook); } catch { /* best effort */ }
                workbook = null;
            }

            // Step 4: Quit Excel application with resilient retry + overall timeout
            if (excel != null)
            {
                int attemptNumber = 0;
                Exception? lastException = null;

                // Outer timeout catches truly hung Excel (modal dialogs, deadlocks)
                // Excel can take a long time to quit after saving large files or when antivirus is scanning
                using var quitTimeout = new CancellationTokenSource(ComInteropConstants.ExcelQuitTimeout);

                try
                {
                    logger.LogDebug("Attempting to quit Excel for {FileName} with resilient retry ({Timeout} timeout)", fileName, ComInteropConstants.ExcelQuitTimeout);

                    // Inner retry pipeline handles transient COM busy errors within the timeout
                    _quitPipeline.Execute(cancellationToken =>
                    {
                        attemptNumber++;
                        try
                        {
                            logger.LogDebug("Quit attempt {Attempt} for {FileName}", attemptNumber, fileName);
                            excel.Quit();
                            logger.LogDebug("Quit attempt {Attempt} succeeded for {FileName}", attemptNumber, fileName);
                        }
                        catch (COMException ex)
                        {
                            lastException = ex;
                            logger.LogWarning(ex,
                                "Quit attempt {Attempt} failed for {FileName} (HResult: 0x{HResult:X8})",
                                attemptNumber, fileName, ex.HResult);
                            throw; // Let pipeline decide if retry
                        }
                    }, quitTimeout.Token);

                    // INSTRUMENTATION: Track successful quit path
                    logger.LogInformation(
                        "[DIAG-QUIT-SUCCESS] Excel quit succeeded for {FileName} after {Attempts} attempt(s) in {Elapsed}ms",
                        fileName, attemptNumber, stopwatch.ElapsedMilliseconds);
                    SessionDiagnostics.WriteStdErr(
                        $"[DIAG-QUIT-SUCCESS] Excel quit succeeded for {fileName} after {attemptNumber} attempt(s) in {stopwatch.ElapsedMilliseconds}ms");
                }
                catch (OperationCanceledException) when (quitTimeout.Token.IsCancellationRequested)
                {
                    // Overall timeout reached - Excel is truly hung
                    // INSTRUMENTATION: Track timeout path
                    logger.LogError(
                        "[DIAG-QUIT-TIMEOUT] Excel quit TIMED OUT after {Timeout} for {FileName} (Attempts: {Attempts}). " +
                        "Excel is likely hung (modal dialog or deadlock). Proceeding with forced COM cleanup.",
                        ComInteropConstants.ExcelQuitTimeout, fileName, attemptNumber);
                    SessionDiagnostics.WriteStdErr(
                        $"[DIAG-QUIT-TIMEOUT] Excel quit TIMED OUT after {ComInteropConstants.ExcelQuitTimeout} for {fileName} (Attempts: {attemptNumber}). Excel is likely hung (modal dialog or deadlock). Proceeding with forced COM cleanup.");
                    lastException = new TimeoutException($"Excel.Quit() timed out after {ComInteropConstants.ExcelQuitTimeout} for {fileName}");
                }
                catch (COMException ex) when (ex.HResult == ResiliencePipelines.RPC_E_CALL_FAILED)
                {
                    // Fatal RPC connection failure - Excel is unreachable
                    // INSTRUMENTATION: Track RPC failure path
                    logger.LogError(ex,
                        "[DIAG-QUIT-RPC-FAILED] Excel RPC connection FAILED (0x800706BE) for {FileName}. " +
                        "Excel is unreachable - this is a FATAL error that cannot be retried. " +
                        "Proceeding with forced COM cleanup. Excel process should be force-killed by caller.",
                        fileName);
                    SessionDiagnostics.WriteStdErr(
                        $"[DIAG-QUIT-RPC-FAILED] Excel RPC connection FAILED (0x800706BE) for {fileName}. Excel is unreachable - this is a FATAL error that cannot be retried. Proceeding with forced COM cleanup. Excel process should be force-killed by caller.");
                    lastException = ex;
                    // Don't retry - RPC connection is dead, Excel is gone
                }
                catch (COMException ex) when (ex.HResult == ResiliencePipelines.RPC_E_DISCONNECTED)
                {
                    logger.LogWarning(ex,
                        "Excel COM proxy was disconnected while calling Quit for {FileName} - proceeding with COM cleanup",
                        fileName);
                    lastException = ex;
                }
                catch (COMException ex)
                {
                    // All retry attempts exhausted or non-retriable error
                    // INSTRUMENTATION: Track retry exhaustion path
                    logger.LogError(ex,
                        "[DIAG-QUIT-RETRY-EXHAUSTED] Excel quit failed for {FileName} after {Attempts} attempt(s) (HResult: 0x{HResult:X8}, Elapsed: {Elapsed}ms) - proceeding with COM cleanup",
                        fileName, attemptNumber, ex.HResult, stopwatch.ElapsedMilliseconds);
                    SessionDiagnostics.WriteStdErr(
                        $"[DIAG-QUIT-RETRY-EXHAUSTED] Excel quit failed for {fileName} after {attemptNumber} attempt(s) (HResult: 0x{ex.HResult:X8}, Elapsed: {stopwatch.ElapsedMilliseconds}ms) - proceeding with COM cleanup");
                    lastException = ex;
                }
                catch (MissingMemberException ex)
                {
                    logger.LogWarning(ex,
                        "Excel COM proxy was disconnected while calling Quit for {FileName} - proceeding with COM cleanup",
                        fileName);
                    lastException = ex;
                }
                finally
                {
                    // Step 5: Release Excel COM reference (even if Quit failed/timed out)
                    Marshal.ReleaseComObject(excel);
                    excel = null;
                }

                // Additional diagnostic if quit failed
                if (lastException != null)
                {
                    // INSTRUMENTATION: Track cleanup after failed quit
                    logger.LogWarning(
                        "[DIAG-QUIT-CLEANUP-AFTER-FAILURE] Excel quit unsuccessful for {FileName} (Elapsed: {Elapsed}s, Type: {ExceptionType}). " +
                        "COM cleanup completed. Process may leak if Excel remains hung.",
                        fileName, stopwatch.Elapsed.TotalSeconds, lastException.GetType().Name);
                    SessionDiagnostics.WriteStdErr(
                        $"[DIAG-QUIT-CLEANUP-AFTER-FAILURE] Excel quit unsuccessful for {fileName} (Elapsed: {stopwatch.Elapsed.TotalSeconds:F3}s, Type: {lastException.GetType().Name}). COM cleanup completed. Process may leak if Excel remains hung.");
                }
            }
        }
        finally
        {
            // Step 6: COM cleanup happens automatically via RCW finalizers
            // Per Microsoft docs: "RCWs can be cleaned by the CLR without additional code"
            // GC.Collect() is rarely needed and can decrease performance
            // https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/induced
            // https://learn.microsoft.com/en-us/dotnet/framework/performance/reliability-best-practices

            logger.LogDebug("Excel shutdown sequence completed for {FileName} in {Elapsed}ms",
                fileName, stopwatch.ElapsedMilliseconds);
        }
    }
}


