using System.Data;
using Dapper;
using PlotLine.Models;

namespace PlotLine.Data;

public interface IStoryIntelligenceResultRepository
{
    Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request);
    Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request);
    Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync();
    Task UpdateProgressAsync(
        int runId,
        string? status = null,
        string? currentStage = null,
        string? currentMessage = null,
        int? totalDetectedScenes = null,
        int? completedScenes = null,
        int? failedScenes = null,
        int? totalInputTokens = null,
        int? totalOutputTokens = null,
        int? totalTokens = null,
        long? totalDurationMs = null,
        decimal? estimatedCostGBP = null,
        decimal? estimatedCostUSD = null);
    Task CompleteRunAsync(int runId, string status, int? totalInputTokens, int? totalOutputTokens, int? totalTokens, long? totalDurationMs, decimal? estimatedCostGBP, decimal? estimatedCostUSD);
    Task FailRunAsync(int runId, string failureStage, string errorMessage, string? errorDetail, long? totalDurationMs);
    Task RequestCancelAsync(int runId);
    Task CancelRunAsync(int runId, long? totalDurationMs);
    Task<int> SaveChapterResultAsync(int runId, StoryIntelligenceChapterResultSaveRequest chapter);
    Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene);
    Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync();
    Task<IReadOnlyList<StoryIntelligenceSavedRun>> ListRunsByBookForUserAsync(int bookId, int userId);
    Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId);
    Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId);
    Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId);
    Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId);
    Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId);
    Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request);
}

public sealed class StoryIntelligenceResultRepository(ISqlConnectionFactory connectionFactory) : IStoryIntelligenceResultRepository
{
    public async Task<int> SaveAsync(StoryIntelligenceRunSaveRequest request)
    {
        using var connection = connectionFactory.CreateConnection();
        connection.Open();
        using var transaction = connection.BeginTransaction();

        try
        {
            var runId = await connection.QuerySingleAsync<int>(
                "dbo.StoryIntelligenceRun_Save",
                new
                {
                    request.UserID,
                    request.ProjectID,
                    request.BookID,
                    request.Status,
                    request.SourceType,
                    request.SourceFileName,
                    request.SourceFileSizeBytes,
                    request.SourceWordCount,
                    request.SourceCharacterCount,
                    request.SourceChapterCount,
                    request.SourceDetectedImagesCount,
                    request.SourceDetectedTablesCount,
                    request.SourceDetectedFootnotesCount,
                    request.SourceDetectedCommentsCount,
                    request.PromptVersion,
                    request.Model,
                    request.StartedUtc,
                    request.CompletedUtc,
                    request.FailureStage,
                    request.TotalInputTokens,
                    request.TotalOutputTokens,
                    request.TotalTokens,
                    request.TotalDurationMs,
                    request.EstimatedCostGBP,
                    request.EstimatedCostUSD,
                    request.PromptVersionsSummary,
                    request.ErrorMessage,
                    request.ErrorDetail
                },
                transaction,
                commandType: CommandType.StoredProcedure);

            int? chapterResultId = null;
            if (request.ChapterResult is not null)
            {
                var chapter = request.ChapterResult;
                chapterResultId = await connection.QuerySingleAsync<int>(
                    "dbo.StoryIntelligenceChapterResult_Save",
                    new
                    {
                        StoryIntelligenceRunID = runId,
                        chapter.ProjectID,
                        chapter.BookID,
                        chapter.ChapterID,
                        chapter.ChapterNumber,
                        chapter.SourceLabel,
                        chapter.PromptVersion,
                        chapter.Model,
                        chapter.RawResponseJson,
                        chapter.OutputTextJson,
                        chapter.ParsedJson,
                        chapter.ValidationErrorsCount,
                        chapter.ValidationWarningsCount,
                        chapter.InputTokens,
                        chapter.OutputTokens,
                        chapter.TotalTokens,
                        chapter.DurationMs
                    },
                    transaction,
                    commandType: CommandType.StoredProcedure);
            }

            foreach (var scene in request.SceneResults)
            {
                await connection.ExecuteAsync(
                    "dbo.StoryIntelligenceSceneResult_Save",
                    new
                    {
                        StoryIntelligenceRunID = runId,
                        ChapterResultID = chapterResultId,
                        scene.ProjectID,
                        scene.BookID,
                        scene.ChapterID,
                        scene.SceneID,
                        scene.TemporarySceneNumber,
                        scene.StartParagraph,
                        scene.EndParagraph,
                        scene.SourceLabel,
                        scene.PromptVersion,
                        scene.Model,
                        scene.RawResponseJson,
                        scene.OutputTextJson,
                        scene.ParsedJson,
                        scene.ValidationErrorsCount,
                        scene.ValidationWarningsCount,
                        scene.InputTokens,
                        scene.OutputTokens,
                        scene.TotalTokens,
                        scene.DurationMs
                    },
                    transaction,
                    commandType: CommandType.StoredProcedure);
            }

            transaction.Commit();
            return runId;
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }

    public async Task<int> QueueAdminTextAsync(StoryIntelligenceRunQueueRequest request)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.StoryIntelligenceRun_QueueAdminText",
            new
            {
                request.UserID,
                request.ProjectID,
                request.BookID,
                request.ChapterID,
                request.ChapterNumber,
                request.SourceType,
                request.SourceLabel,
                SourceText = request.ChapterText,
                request.SourceFileName,
                request.SourceFileSizeBytes,
                request.SourceWordCount,
                request.SourceCharacterCount,
                request.SourceParagraphCount,
                request.SourceChapterCount,
                request.Model,
                request.PromptVersionsSummary
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<StoryIntelligenceQueuedRun?> ClaimNextPendingAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceQueuedRun>(
            "dbo.StoryIntelligenceRun_ClaimNextPending",
            commandType: CommandType.StoredProcedure);
    }

    public async Task UpdateProgressAsync(
        int runId,
        string? status = null,
        string? currentStage = null,
        string? currentMessage = null,
        int? totalDetectedScenes = null,
        int? completedScenes = null,
        int? failedScenes = null,
        int? totalInputTokens = null,
        int? totalOutputTokens = null,
        int? totalTokens = null,
        long? totalDurationMs = null,
        decimal? estimatedCostGBP = null,
        decimal? estimatedCostUSD = null)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryIntelligenceRun_UpdateProgress",
            new
            {
                StoryIntelligenceRunID = runId,
                Status = status,
                CurrentStage = currentStage,
                CurrentMessage = currentMessage,
                TotalDetectedScenes = totalDetectedScenes,
                CompletedScenes = completedScenes,
                FailedScenes = failedScenes,
                TotalInputTokens = totalInputTokens,
                TotalOutputTokens = totalOutputTokens,
                TotalTokens = totalTokens,
                TotalDurationMs = totalDurationMs,
                EstimatedCostGBP = estimatedCostGBP,
                EstimatedCostUSD = estimatedCostUSD
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task CompleteRunAsync(int runId, string status, int? totalInputTokens, int? totalOutputTokens, int? totalTokens, long? totalDurationMs, decimal? estimatedCostGBP, decimal? estimatedCostUSD)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryIntelligenceRun_Complete",
            new
            {
                StoryIntelligenceRunID = runId,
                Status = status,
                TotalInputTokens = totalInputTokens,
                TotalOutputTokens = totalOutputTokens,
                TotalTokens = totalTokens,
                TotalDurationMs = totalDurationMs,
                EstimatedCostGBP = estimatedCostGBP,
                EstimatedCostUSD = estimatedCostUSD
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task FailRunAsync(int runId, string failureStage, string errorMessage, string? errorDetail, long? totalDurationMs)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryIntelligenceRun_Fail",
            new
            {
                StoryIntelligenceRunID = runId,
                FailureStage = failureStage,
                ErrorMessage = errorMessage,
                ErrorDetail = errorDetail,
                TotalDurationMs = totalDurationMs
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task RequestCancelAsync(int runId)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryIntelligenceRun_RequestCancel",
            new { StoryIntelligenceRunID = runId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task CancelRunAsync(int runId, long? totalDurationMs)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryIntelligenceRun_Cancel",
            new { StoryIntelligenceRunID = runId, TotalDurationMs = totalDurationMs },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveChapterResultAsync(int runId, StoryIntelligenceChapterResultSaveRequest chapter)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.StoryIntelligenceChapterResult_Save",
            new
            {
                StoryIntelligenceRunID = runId,
                chapter.ProjectID,
                chapter.BookID,
                chapter.ChapterID,
                chapter.ChapterNumber,
                chapter.SourceLabel,
                chapter.PromptVersion,
                chapter.Model,
                chapter.RawResponseJson,
                chapter.OutputTextJson,
                chapter.ParsedJson,
                chapter.ValidationErrorsCount,
                chapter.ValidationWarningsCount,
                chapter.InputTokens,
                chapter.OutputTokens,
                chapter.TotalTokens,
                chapter.DurationMs
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<int> SaveSceneResultAsync(int runId, int? chapterResultId, StoryIntelligenceSceneResultSaveRequest scene)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<int>(
            "dbo.StoryIntelligenceSceneResult_Save",
            new
            {
                StoryIntelligenceRunID = runId,
                ChapterResultID = chapterResultId,
                scene.ProjectID,
                scene.BookID,
                scene.ChapterID,
                scene.SceneID,
                scene.TemporarySceneNumber,
                scene.StartParagraph,
                scene.EndParagraph,
                scene.SourceLabel,
                scene.PromptVersion,
                scene.Model,
                scene.RawResponseJson,
                scene.OutputTextJson,
                scene.ParsedJson,
                scene.ValidationErrorsCount,
                scene.ValidationWarningsCount,
                scene.InputTokens,
                scene.OutputTokens,
                scene.TotalTokens,
                scene.DurationMs
            },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<StoryIntelligenceSavedRunListItem>> ListRunsAsync()
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryIntelligenceSavedRunListItem>(
            "dbo.StoryIntelligenceRun_ListAdmin",
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<IReadOnlyList<StoryIntelligenceSavedRun>> ListRunsByBookForUserAsync(int bookId, int userId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryIntelligenceSavedRun>(
            "dbo.StoryIntelligenceRun_ListByBookForUser",
            new { BookID = bookId, UserID = userId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<StoryIntelligenceSavedRun?> GetRunAsync(int runId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSavedRun>(
            "dbo.StoryIntelligenceRun_GetAdmin",
            new { StoryIntelligenceRunID = runId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<StoryIntelligenceSavedChapterResult?> GetChapterResultAsync(int runId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceSavedChapterResult>(
            "dbo.StoryIntelligenceChapterResult_GetByRun",
            new { StoryIntelligenceRunID = runId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<IReadOnlyList<StoryIntelligenceSavedSceneResult>> ListSceneResultsAsync(int runId)
    {
        using var connection = connectionFactory.CreateConnection();
        var rows = await connection.QueryAsync<StoryIntelligenceSavedSceneResult>(
            "dbo.StoryIntelligenceSceneResult_ListByRun",
            new { StoryIntelligenceRunID = runId },
            commandType: CommandType.StoredProcedure);
        return rows.ToList();
    }

    public async Task<StoryIntelligenceImportCommit?> GetImportCommitAsync(int runId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleOrDefaultAsync<StoryIntelligenceImportCommit>(
            "dbo.StoryIntelligenceImportCommit_GetByRun",
            new { StoryIntelligenceRunID = runId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<StoryIntelligenceChapterImportReadiness> GetChapterImportReadinessAsync(int chapterId)
    {
        using var connection = connectionFactory.CreateConnection();
        return await connection.QuerySingleAsync<StoryIntelligenceChapterImportReadiness>(
            "dbo.StoryIntelligenceImportCommit_GetChapterReadiness",
            new { ChapterID = chapterId },
            commandType: CommandType.StoredProcedure);
    }

    public async Task<StoryIntelligenceImportCommitResult> CommitImportAsync(StoryIntelligenceImportCommitRequest request)
    {
        using var connection = connectionFactory.CreateConnection();
        connection.Open();
        using var transaction = connection.BeginTransaction();

        var scenesCreated = 0;
        var metricsCreated = 0;
        var commitWarnings = new List<string>();
        try
        {
            var alreadyCommitted = await connection.QuerySingleAsync<bool>(
                "dbo.StoryIntelligenceImportCommit_HasCompleted",
                new { request.StoryIntelligenceRunID },
                transaction,
                commandType: CommandType.StoredProcedure);
            if (alreadyCommitted)
            {
                transaction.Rollback();
                return new StoryIntelligenceImportCommitResult
                {
                    Success = false,
                    Message = "This Story Intelligence run has already been committed."
                };
            }

            var readiness = await connection.QuerySingleAsync<StoryIntelligenceChapterImportReadiness>(
                "dbo.StoryIntelligenceImportCommit_GetChapterReadiness",
                new { request.ChapterID },
                transaction,
                commandType: CommandType.StoredProcedure);
            if (!readiness.CanCommit)
            {
                transaction.Rollback();
                return new StoryIntelligenceImportCommitResult
                {
                    Success = false,
                    Message = readiness.BlockReason ?? "This chapter already contains scenes."
                };
            }

            if (!string.IsNullOrWhiteSpace(request.ChapterSummary) || request.ChapterPurposeID.HasValue)
            {
                await connection.ExecuteAsync(
                    "dbo.StoryIntelligenceImportCommit_UpdateChapterPlanning",
                    new
                    {
                        request.ChapterID,
                        Summary = request.ChapterSummary,
                        request.ChapterPurposeID
                    },
                    transaction,
                    commandType: CommandType.StoredProcedure);
            }

            var orderedScenes = request.Scenes.OrderBy(scene => scene.TemporarySceneNumber).ToList();
            for (var index = 0; index < orderedScenes.Count; index++)
            {
                var item = orderedScenes[index];
                var operation = "preparing scene";

                try
                {
                    operation = "saving scene";
                    var relativeTimeText = TrimOptionalText(
                        item.ParsedScene.Setting?.DateOrTimeReference ?? item.ParsedScene.Setting?.TimeOfDay,
                        200,
                        $"Scene {item.TemporarySceneNumber:N0} relative time",
                        commitWarnings);

                    var sceneTitle = TrimOptionalText(item.SceneTitle, 200, $"Scene {item.TemporarySceneNumber:N0} title", commitWarnings)
                        ?? $"Scene {item.TemporarySceneNumber:N0}";

                    var sceneId = await connection.QuerySingleAsync<int>(
                        "dbo.Scene_Save",
                        new
                        {
                            SceneID = (int?)null,
                            ChapterID = request.ChapterID,
                            SceneNumber = Convert.ToDecimal(item.TemporarySceneNumber),
                            SceneTitle = sceneTitle,
                            Summary = item.Summary,
                            TimeModeID = request.TimeModeID,
                            StartDateTime = (DateTime?)null,
                            EndDateTime = (DateTime?)null,
                            DurationAmount = (decimal?)null,
                            DurationUnitID = (int?)null,
                            RelativeTimeText = relativeTimeText,
                            TimeConfidenceID = request.TimeConfidenceID,
                            ScenePurposeNotes = item.PurposeNotes,
                            SceneOutcomeNotes = item.OutcomeNotes,
                            RevisionStatusID = request.RevisionStatusID,
                            PrimaryLocationID = (int?)null,
                            FloorPlanID = (int?)null,
                            InitialFloorPlanFloorID = (int?)null
                        },
                        transaction,
                        commandType: CommandType.StoredProcedure);

                    operation = "saving source tracking";
                    await connection.ExecuteAsync(
                        "dbo.StoryIntelligenceSceneSource_Save",
                        new
                        {
                            SceneID = sceneId,
                            ImportSource = "Story Intelligence",
                            ImportRunID = request.StoryIntelligenceRunID,
                            SourceStartParagraph = item.StartParagraph,
                            SourceEndParagraph = item.EndParagraph
                        },
                        transaction,
                        commandType: CommandType.StoredProcedure);

                    if (item.PovCharacterID.HasValue)
                    {
                        operation = "linking POV character";
                        await TryOptionalSceneOperationAsync(
                            connection,
                            transaction,
                            item.TemporarySceneNumber,
                            operation,
                            commitWarnings,
                            () => connection.ExecuteAsync(
                                "dbo.Scene_UpdatePovCharacter",
                                new { SceneID = sceneId, POVCharacterID = item.PovCharacterID },
                                transaction,
                                commandType: CommandType.StoredProcedure));
                    }

                    if (item.PurposeTypeIDs.Count > 0)
                    {
                        operation = "linking purpose labels";
                        await TryOptionalSceneOperationAsync(
                            connection,
                            transaction,
                            item.TemporarySceneNumber,
                            operation,
                            commitWarnings,
                            () => connection.ExecuteAsync(
                                "dbo.Scene_SavePurposes",
                                new { SceneID = sceneId, PurposeIds = string.Join(',', item.PurposeTypeIDs.Distinct()) },
                                transaction,
                                commandType: CommandType.StoredProcedure));
                    }

                    foreach (var metric in item.Metrics)
                    {
                        operation = $"saving metric '{metric.MetricName}'";
                        var saved = await TryOptionalSceneOperationAsync(
                            connection,
                            transaction,
                            item.TemporarySceneNumber,
                            operation,
                            commitWarnings,
                            () => connection.ExecuteAsync(
                                "dbo.SceneMetric_SaveValue",
                                new { SceneID = sceneId, metric.MetricTypeID, metric.Value, metric.Notes },
                                transaction,
                                commandType: CommandType.StoredProcedure));
                        if (saved)
                        {
                            metricsCreated++;
                        }
                    }

                    if (request.TimelineNoteTypeID.HasValue && !string.IsNullOrWhiteSpace(item.ImportNoteText))
                    {
                        operation = "saving import note";
                        await TryOptionalSceneOperationAsync(
                            connection,
                            transaction,
                            item.TemporarySceneNumber,
                            operation,
                            commitWarnings,
                            () => connection.ExecuteAsync(
                                "dbo.SceneNote_Save",
                                new
                                {
                                    SceneNoteID = (int?)null,
                                    SceneID = sceneId,
                                    SceneNoteTypeID = request.TimelineNoteTypeID.Value,
                                    NoteTitle = "Story Intelligence import notes",
                                    NoteText = item.ImportNoteText,
                                    SortOrder = 10,
                                    IsPinned = false,
                                    IsResolved = false
                                },
                                transaction,
                                commandType: CommandType.StoredProcedure));
                    }

                    scenesCreated++;
                }
                catch (Exception ex)
                {
                    throw new InvalidOperationException(
                        $"Scene {item.TemporarySceneNumber:N0} failed during {operation}: {ex.Message}",
                        ex);
                }
            }

            var notes = new List<string>();
            notes.Add("Committed into empty chapter.");
            notes.AddRange(request.Warnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));
            notes.AddRange(commitWarnings.Where(warning => !string.IsNullOrWhiteSpace(warning)));

            var commitId = await connection.QuerySingleAsync<int>(
                "dbo.StoryIntelligenceImportCommit_RecordCompleted",
                new
                {
                    request.StoryIntelligenceRunID,
                    request.ProjectID,
                    request.BookID,
                    request.ChapterID,
                    CommittedByUserID = request.UserID,
                    ScenesCreated = scenesCreated,
                    MetricsCreated = metricsCreated,
                    Notes = string.Join(Environment.NewLine, notes)
                },
                transaction,
                commandType: CommandType.StoredProcedure);

            transaction.Commit();
            return new StoryIntelligenceImportCommitResult
            {
                Success = true,
                CommitID = commitId,
                ScenesCreated = scenesCreated,
                MetricsCreated = metricsCreated,
                Message = $"Created {scenesCreated:N0} scene(s)."
            };
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            await RecordFailedCommitAsync(request, 0, 0, ex);
            throw;
        }
    }

    private async Task RecordFailedCommitAsync(StoryIntelligenceImportCommitRequest request, int scenesCreated, int metricsCreated, Exception ex)
    {
        using var connection = connectionFactory.CreateConnection();
        await connection.ExecuteAsync(
            "dbo.StoryIntelligenceImportCommit_RecordFailed",
            new
            {
                request.StoryIntelligenceRunID,
                request.ProjectID,
                request.BookID,
                request.ChapterID,
                CommittedByUserID = request.UserID,
                ScenesCreated = scenesCreated,
                MetricsCreated = metricsCreated,
                Notes = ex.Message
            },
            commandType: CommandType.StoredProcedure);
    }

    private static async Task<bool> TryOptionalSceneOperationAsync(
        IDbConnection connection,
        IDbTransaction transaction,
        int sceneNumber,
        string operation,
        ICollection<string> warnings,
        Func<Task<int>> action)
    {
        try
        {
            await action();
            return true;
        }
        catch (Exception ex)
        {
            warnings.Add($"Scene {sceneNumber:N0}: skipped optional {operation} because the database rejected it: {ex.Message}");
            return false;
        }
    }

    private static string? TrimOptionalText(string? value, int maxLength, string label, ICollection<string> warnings)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return null;
        }

        var trimmed = value.Trim();
        if (trimmed.Length <= maxLength)
        {
            return trimmed;
        }

        warnings.Add($"{label} was truncated to {maxLength:N0} characters for the database field limit.");
        return trimmed[..maxLength].TrimEnd();
    }
}
