using Microsoft.AspNetCore.SignalR;
using PlotLine.Data;
using PlotLine.Hubs;
using PlotLine.Models;
using PlotLine.ViewModels;

namespace PlotLine.Services;

public interface IStoryIntelligenceService
{
    Task<StoryIntelligenceOverviewViewModel> GetOverviewAsync();
    Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid jobId);
    Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid jobId);
    Task<StoryIntelligenceJob?> StartAsync();
    Task<StoryIntelligenceJob?> CancelAsync(Guid jobId);
    Task<StoryIntelligenceDashboardViewModel> GetDashboardAsync();
    Task<StoryIntelligenceJobProgress?> GetProgressForUserAsync(int userId, Guid jobId);
}

public interface IStoryIntelligenceProvider
{
    Task ProcessAsync(StoryIntelligenceJob job, Func<StoryIntelligenceProviderProgress, Task> progress, CancellationToken cancellationToken);
}

public sealed class StoryIntelligenceProviderProgress
{
    public string Status { get; init; } = StoryIntelligenceJobStatuses.Preparing;
    public string Stage { get; init; } = StoryIntelligenceJobStatuses.Preparing;
    public string Message { get; init; } = "Preparing manuscript...";
    public int Percent { get; init; }
    public string? EstimatedRemaining { get; init; }
}

public sealed class StubStoryIntelligenceProvider : IStoryIntelligenceProvider
{
    public async Task ProcessAsync(StoryIntelligenceJob job, Func<StoryIntelligenceProviderProgress, Task> progress, CancellationToken cancellationToken)
    {
        var stages = new[]
        {
            new StoryIntelligenceProviderProgress
            {
                Percent = 20,
                Message = "Building analysis pipeline...",
                EstimatedRemaining = "Calculating"
            },
            new StoryIntelligenceProviderProgress
            {
                Percent = 55,
                Message = "Preparing scene batches...",
                EstimatedRemaining = "Calculating"
            },
            new StoryIntelligenceProviderProgress
            {
                Percent = 85,
                Message = "Initialising Story Intelligence...",
                EstimatedRemaining = "Calculating"
            }
        };

        foreach (var stage in stages)
        {
            cancellationToken.ThrowIfCancellationRequested();
            await progress(stage);
            await Task.Yield();
        }
    }
}

public sealed class StoryIntelligenceService(
    IStoryIntelligenceRepository repository,
    IStoryIntelligenceResultRepository persistedRuns,
    IStoryIntelligencePipelineStateService pipelineState,
    IOnboardingStoryIntelligenceBatchStore onboardingBatches,
    IOnboardingRepository onboarding,
    IProjectRepository projects,
    IBookRepository books,
    ICurrentUserService currentUser) : IStoryIntelligenceService
{
    private const string ConsentText = "I consent to PlotDirector preparing the Story Intelligence analysis framework for this manuscript. I understand future AI analysis is optional and will use the OpenAI API.";

    public async Task<StoryIntelligenceOverviewViewModel> GetOverviewAsync()
    {
        var state = await onboarding.GetOrCreateAsync(RequireUserId());
        var latest = await repository.GetLatestForUserAsync(state.UserID);
        return new StoryIntelligenceOverviewViewModel
        {
            ProjectID = state.ProjectID,
            BookID = state.BookID,
            SelectedProjectName = state.ProjectID.HasValue ? (await projects.GetForUserAsync(state.ProjectID.Value, state.UserID))?.ProjectName : null,
            SelectedBookTitle = state.BookID.HasValue ? BuildBookTitle(await books.GetAsync(state.BookID.Value)) : null,
            ExistingJob = latest is null ? null : ToProgress(latest)
        };
    }

    public async Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid jobId)
    {
        var job = await repository.GetAsync(RequireUserId(), jobId);
        return job is null
            ? null
            : new StoryIntelligenceProgressViewModel
            {
                Job = ToProgress(job),
                ProjectName = (await projects.GetForUserAsync(job.ProjectID, job.UserID))?.ProjectName,
                BookTitle = BuildBookTitle(await books.GetAsync(job.BookID))
            };
    }

    public async Task<StoryIntelligenceCompletionViewModel?> GetCompletionAsync(Guid jobId)
    {
        var job = await repository.GetAsync(RequireUserId(), jobId);
        if (job is null || !job.IsCompleted)
        {
            return null;
        }

        return new StoryIntelligenceCompletionViewModel
        {
            Job = ToProgress(job),
            ProjectID = job.ProjectID,
            BookID = job.BookID,
            ProjectName = (await projects.GetForUserAsync(job.ProjectID, job.UserID))?.ProjectName,
            BookTitle = BuildBookTitle(await books.GetAsync(job.BookID))
        };
    }

    public async Task<StoryIntelligenceJob?> StartAsync()
    {
        var state = await onboarding.GetOrCreateAsync(RequireUserId());
        if (!state.ProjectID.HasValue || !state.BookID.HasValue)
        {
            return null;
        }

        return await repository.CreateWithConsentAsync(
            state.UserID,
            state.UserOnboardingStateID,
            state.ProjectID.Value,
            state.BookID.Value,
            ConsentText);
    }

    public async Task<StoryIntelligenceJob?> CancelAsync(Guid jobId)
        => await repository.CancelAsync(RequireUserId(), jobId);

    public async Task<StoryIntelligenceDashboardViewModel> GetDashboardAsync()
    {
        if (!currentUser.UserId.HasValue)
        {
            return new StoryIntelligenceDashboardViewModel();
        }

        var pipelineBooks = (await pipelineState.ListForUserAsync(currentUser.UserId.Value))
            .Select(ToPipelineBookStatus)
            .ToList();
        var activeBatch = await onboardingBatches.GetLatestActiveForUserAsync(currentUser.UserId.Value);
        if (activeBatch is not null)
        {
            var runs = new List<StoryIntelligenceSavedRun>();
            foreach (var item in activeBatch.Items)
            {
                var run = await persistedRuns.GetRunAsync(item.RunID);
                if (run is not null)
                {
                    runs.Add(run);
                }
            }

            var activeRun = runs
                .Where(run => run.Status is StoryIntelligenceRunStatuses.Pending or StoryIntelligenceRunStatuses.Running)
                .OrderBy(run => run.ChapterNumber ?? decimal.MaxValue)
                .ThenBy(run => run.StoryIntelligenceRunID)
                .FirstOrDefault();
            if (activeRun is not null)
            {
                var completedChapters = runs.Count(run => run.Status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings);
                var currentChapter = Math.Clamp(completedChapters + 1, 1, Math.Max(1, activeBatch.Items.Count));
                return new StoryIntelligenceDashboardViewModel
                {
                    ShouldShow = true,
                    ActiveBatchID = activeBatch.BatchID,
                    ActiveImportBookTitle = activeBatch.BookTitle,
                    ActiveImportProgressLabel = $"Chapter {currentChapter:N0} of {activeBatch.Items.Count:N0}",
                    Title = "Story Intelligence is reading",
                    Description = "PlotDirector is analysing your manuscript in the background.",
                    ButtonText = "View progress",
                    PipelineBooks = pipelineBooks
                };
            }
        }

        var nextPipeline = pipelineBooks.FirstOrDefault(item => !item.IsComplete) ?? pipelineBooks.FirstOrDefault();
        if (nextPipeline is not null)
        {
            return new StoryIntelligenceDashboardViewModel
            {
                ShouldShow = true,
                Title = nextPipeline.IsComplete ? "Story Intelligence Complete" : "Continue Story Intelligence",
                Description = nextPipeline.IsComplete
                    ? $"{nextPipeline.BookTitle} has completed the currently available Story Intelligence stages."
                    : $"{nextPipeline.BookTitle} is waiting at the next Story Intelligence review stage.",
                ButtonText = nextPipeline.IsComplete ? "View summary" : "Continue Story Intelligence",
                PipelineBooks = pipelineBooks
            };
        }

        var state = await onboarding.GetAsync(currentUser.UserId.Value);
        return state?.ProjectID.HasValue == true && state.BookID.HasValue
            ? new StoryIntelligenceDashboardViewModel
            {
                ShouldShow = true,
                Title = "Import your manuscript",
                Description = "Scan your manuscript with the Word Companion, review the chapters, then let PlotDirector prepare scene suggestions.",
                ButtonText = "Continue manuscript import",
                PipelineBooks = pipelineBooks
            }
            : new StoryIntelligenceDashboardViewModel();
    }

    public async Task<StoryIntelligenceJobProgress?> GetProgressForUserAsync(int userId, Guid jobId)
    {
        var job = await repository.GetAsync(userId, jobId);
        return job is null ? null : ToProgress(job);
    }

    public static StoryIntelligenceJobProgress ToProgress(StoryIntelligenceJob job)
    {
        var elapsedFrom = job.StartedUtc ?? job.CreatedUtc;
        return new StoryIntelligenceJobProgress
        {
            JobID = job.JobID,
            UserID = job.UserID,
            ProjectID = job.ProjectID,
            BookID = job.BookID,
            Status = job.Status,
            CurrentStage = job.CurrentStage,
            CurrentMessage = job.CurrentMessage,
            ProgressPercent = job.ProgressPercent,
            EstimatedRemaining = job.EstimatedRemaining,
            ErrorMessage = job.ErrorMessage,
            UpdatedUtc = job.UpdatedUtc,
            IsActive = job.IsActive,
            IsCompleted = job.IsCompleted,
            ElapsedTime = FormatElapsed(DateTime.UtcNow - elapsedFrom)
        };
    }

    private int RequireUserId()
        => currentUser.UserId ?? throw new InvalidOperationException("Sign in to use Story Intelligence.");

    private static string BuildBookTitle(Book? book)
        => book is null ? "Selected book" : $"Book {book.BookNumber}: {book.BookDisplayTitle}";

    private static StoryIntelligencePipelineBookStatusViewModel ToPipelineBookStatus(StoryIntelligenceBookPipelineState state)
        => new()
        {
            BookID = state.BookID,
            BookTitle = state.BookDisplayTitle,
            StatusLabel = PipelineStatusLabel(state),
            Description = PipelineDescription(state),
            IsComplete = state.IsComplete
        };

    private static string PipelineStatusLabel(StoryIntelligenceBookPipelineState state)
    {
        if (state.IsComplete)
        {
            return "Story Intelligence Complete";
        }

        return state.CurrentReviewStage switch
        {
            StoryIntelligencePipelineStages.KnowledgeReview => "Waiting for Knowledge Review",
            StoryIntelligencePipelineStages.RelationshipReview => "Waiting for Relationship Review",
            StoryIntelligencePipelineStages.AssetReview => "Waiting for Asset Review",
            StoryIntelligencePipelineStages.LocationReview => "Waiting for Location Review",
            StoryIntelligencePipelineStages.CharacterReview => "Waiting for Character Review",
            StoryIntelligencePipelineStages.SceneReview => "Needs Scene Review",
            _ when StoryIntelligenceReadyForKnowledge(state) => "Waiting for Knowledge Review",
            _ when StoryIntelligenceReadyForRelationships(state) => "Waiting for Relationship Review",
            _ when StoryIntelligenceReadyForAssets(state) => "Waiting for Asset Review",
            _ when StoryIntelligenceReadyForLocations(state) => "Waiting for Location Review",
            _ => string.Equals(state.Status, StoryIntelligencePipelineStatuses.InProgress, StringComparison.OrdinalIgnoreCase)
                ? "Story Intelligence In Progress"
                : "Story Intelligence Needs Review"
        };
    }

    private static string PipelineDescription(StoryIntelligenceBookPipelineState state)
        => state.IsComplete
            ? "All currently available Story Intelligence stages are complete."
            : state.CurrentReviewStage switch
            {
                StoryIntelligencePipelineStages.KnowledgeReview => "Review detected knowledge changes and decide what to create or link.",
                StoryIntelligencePipelineStages.RelationshipReview => "Review detected relationships and decide what to create or link.",
                StoryIntelligencePipelineStages.AssetReview => "Review detected assets and decide what to create or link.",
                StoryIntelligencePipelineStages.LocationReview => "Review detected locations and decide what to create or link.",
                StoryIntelligencePipelineStages.CharacterReview => "Review detected characters and decide what to create or link.",
                StoryIntelligencePipelineStages.SceneReview => "Review prepared scenes before creating them.",
                _ when StoryIntelligenceReadyForKnowledge(state) => "Review detected knowledge changes and decide what to create or link.",
                _ when StoryIntelligenceReadyForRelationships(state) => "Review detected relationships and decide what to create or link.",
                _ when StoryIntelligenceReadyForAssets(state) => "Review detected assets and decide what to create or link.",
                _ when StoryIntelligenceReadyForLocations(state) => "Review detected locations and decide what to create or link.",
                _ => "Continue from the next Story Intelligence stage."
            };

    private static bool StoryIntelligenceReadyForKnowledge(StoryIntelligenceBookPipelineState state)
        => string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.RelationshipImport, StringComparison.OrdinalIgnoreCase)
            || state.CurrentStage is StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport;

    private static bool StoryIntelligenceReadyForRelationships(StoryIntelligenceBookPipelineState state)
        => string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.AssetImport, StringComparison.OrdinalIgnoreCase)
            || state.CurrentStage is StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
                or StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport;

    private static bool StoryIntelligenceReadyForAssets(StoryIntelligenceBookPipelineState state)
        => string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.LocationImport, StringComparison.OrdinalIgnoreCase)
            || state.CurrentStage is StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
                or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
                or StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport
            || (string.Equals(state.CurrentStage, StoryIntelligencePipelineStages.Complete, StringComparison.OrdinalIgnoreCase)
                && !string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.KnowledgeImport, StringComparison.OrdinalIgnoreCase));

    private static bool StoryIntelligenceReadyForLocations(StoryIntelligenceBookPipelineState state)
        => string.Equals(state.LastCompletedStage, StoryIntelligencePipelineStages.CharacterImport, StringComparison.OrdinalIgnoreCase)
            || state.CurrentStage is StoryIntelligencePipelineStages.LocationReview or StoryIntelligencePipelineStages.LocationImport
                or StoryIntelligencePipelineStages.AssetReview or StoryIntelligencePipelineStages.AssetImport
                or StoryIntelligencePipelineStages.RelationshipReview or StoryIntelligencePipelineStages.RelationshipImport
                or StoryIntelligencePipelineStages.KnowledgeReview or StoryIntelligencePipelineStages.KnowledgeImport;

    private static string FormatElapsed(TimeSpan elapsed)
        => elapsed.TotalMinutes < 1
            ? $"{Math.Max(0, (int)elapsed.TotalSeconds)} sec"
            : $"{(int)elapsed.TotalMinutes} min {elapsed.Seconds:D2} sec";
}

public sealed class StoryIntelligenceWorker(
    IServiceScopeFactory scopeFactory,
    IHubContext<StoryIntelligenceHub> hub,
    ILogger<StoryIntelligenceWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromSeconds(2));
        while (!stoppingToken.IsCancellationRequested)
        {
            await ProcessNextAsync(stoppingToken);
            await timer.WaitForNextTickAsync(stoppingToken);
        }
    }

    private async Task ProcessNextAsync(CancellationToken stoppingToken)
    {
        using var scope = scopeFactory.CreateScope();
        var repository = scope.ServiceProvider.GetRequiredService<IStoryIntelligenceRepository>();
        var provider = scope.ServiceProvider.GetRequiredService<IStoryIntelligenceProvider>();
        var job = await repository.ClaimNextPendingAsync();
        if (job is null)
        {
            return;
        }

        await NotifyAsync(job);
        try
        {
            await provider.ProcessAsync(job, async update =>
            {
                var updated = await repository.UpdateProgressAsync(
                    job.JobID,
                    update.Status,
                    update.Percent,
                    update.Stage,
                    update.Message,
                    update.EstimatedRemaining);
                if (updated is not null)
                {
                    await NotifyAsync(updated);
                    if (updated.IsCancelled)
                    {
                        throw new OperationCanceledException();
                    }
                }
            }, stoppingToken);

            var completed = await repository.CompleteAsync(job.JobID);
            if (completed is not null)
            {
                await NotifyAsync(completed);
            }
        }
        catch (OperationCanceledException) when (!stoppingToken.IsCancellationRequested)
        {
            var cancelled = await repository.CancelAsync(job.UserID, job.JobID);
            if (cancelled is not null)
            {
                await NotifyAsync(cancelled);
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Story Intelligence job {JobID} failed.", job.JobID);
            var failed = await repository.FailAsync(job.JobID, ex.Message);
            if (failed is not null)
            {
                await NotifyAsync(failed);
            }
        }
    }

    private Task NotifyAsync(StoryIntelligenceJob job)
        => hub.Clients.Group(StoryIntelligenceHub.UserGroup(job.UserID))
            .SendAsync("StoryIntelligenceProgressChanged", StoryIntelligenceService.ToProgress(job));
}
