using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using System.Security.Claims;

namespace PlotLine.Hubs;

[Authorize]
public sealed class WordCompanionFollowHub(
    IWordCompanionPresenceService presence,
    IOnboardingRepository onboarding,
    IManuscriptScanPreviewStore scanStore,
    IOnboardingService onboardingService) : Hub
{
    public override async Task OnConnectedAsync()
    {
        if (TryGetUserId(out var userId))
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, PresenceGroup(userId));
        }

        await base.OnConnectedAsync();
    }

    public async Task<WordCompanionPresenceStatus> WatchCompanionPresence()
    {
        var userId = RequireUserId();
        await Groups.AddToGroupAsync(Context.ConnectionId, PresenceGroup(userId));
        return await presence.GetStatusAsync(userId);
    }

    public async Task<WordCompanionPresenceStatus> RegisterCompanion(WordCompanionPresenceRegistration registration)
    {
        var userId = RequireUserId();
        if (registration.UserID.HasValue && registration.UserID.Value != userId)
        {
            throw new HubException("The Companion is signed in as a different user.");
        }

        var status = await presence.RegisterAsync(userId, Context.ConnectionId, registration);
        await Clients.Group(PresenceGroup(userId)).SendAsync("WordCompanionPresenceChanged", status);
        return status;
    }

    public async Task<WordCompanionPresenceStatus> CompanionHeartbeat(WordCompanionPresenceHeartbeat heartbeat)
    {
        var userId = RequireUserId();
        var status = await presence.HeartbeatAsync(userId, Context.ConnectionId, heartbeat);
        await Clients.Group(PresenceGroup(userId)).SendAsync("WordCompanionPresenceChanged", status);
        return status;
    }

    public async Task<ManuscriptScanState> StartOnboardingManuscriptScan()
    {
        var userId = RequireUserId();
        var state = await onboarding.GetAsync(userId);
        if (state is null
            || !IsMicrosoftWordState(state)
            || !state.ProjectID.HasValue
            || !state.BookID.HasValue)
        {
            throw new HubException("Finish choosing a Project and Book before scanning.");
        }

        var presenceStatus = await presence.GetStatusAsync(userId);
        if (!presenceStatus.IsConnected)
        {
            throw new HubException("The Word Companion is not connected. Open Word and try again.");
        }

        if (!presenceStatus.DocumentOpen)
        {
            throw new HubException("Open your manuscript in Word before scanning.");
        }

        var companionConnectionId = await presence.GetCompanionConnectionIdAsync(userId);
        if (string.IsNullOrWhiteSpace(companionConnectionId))
        {
            throw new HubException("The Word Companion connection is not ready. Try reconnecting the Companion.");
        }

        var scanState = await scanStore.StartAsync(userId, state.UserOnboardingStateID, state.ProjectID.Value, state.BookID.Value);
        await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanStateChanged", scanState);
        await Clients.Client(companionConnectionId).SendAsync("ScanCurrentDocument", new ManuscriptScanCommand
        {
            UserID = userId,
            OnboardingID = state.UserOnboardingStateID,
            ProjectID = state.ProjectID.Value,
            BookID = state.BookID.Value,
            RequestedUtc = DateTime.UtcNow
        });
        return scanState;
    }

    public async Task<ManuscriptScanState> ReportOnboardingScanProgress(ManuscriptScanProgress progress)
    {
        var userId = RequireUserId();
        var scanState = await scanStore.ProgressAsync(userId, progress);
        await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanProgress", scanState);
        return scanState;
    }

    public async Task<ManuscriptScanState> CompleteOnboardingScan(ManuscriptScanPreview preview)
    {
        var userId = RequireUserId();
        var state = await onboarding.GetAsync(userId);
        if (state is not null)
        {
            preview = NormalisePreviewContext(preview, userId, state);
        }

        if (state is null
            || state.UserOnboardingStateID != preview.OnboardingID
            || state.ProjectID != preview.ProjectID
            || state.BookID != preview.BookID)
        {
            throw new HubException("The scan result does not match your onboarding setup.");
        }

        var scanState = await scanStore.CompleteAsync(userId, preview);
        await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanCompleted", scanState);
        return scanState;
    }

    public async Task<ManuscriptScanState> FailOnboardingScan(ManuscriptScanFailure failure)
    {
        var userId = RequireUserId();
        var scanState = await scanStore.FailAsync(userId, failure.OnboardingID, failure.Message);
        await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingScanFailed", scanState);
        return scanState;
    }

    public async Task<OnboardingManuscriptBuildResult> StartOnboardingProjectBuild(Guid previewId)
    {
        var userId = RequireUserId();
        await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", new OnboardingBuildProgress
        {
            Status = "Running",
            Message = "Creating chapters...",
            PercentComplete = 0,
            PreviewID = previewId
        });

        var result = await onboardingService.BuildApprovedStructureAsync(
            previewId,
            progress => Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildProgress", progress));
        if (result is null)
        {
            throw new HubException("The reviewed scan could not be found.");
        }

        var companionConnectionId = await presence.GetCompanionConnectionIdAsync(userId);
        if (!string.IsNullOrWhiteSpace(companionConnectionId))
        {
            await Clients.Client(companionConnectionId).SendAsync("UpdateOnboardingBuildMarkers", result);
        }

        await Clients.Group(PresenceGroup(userId)).SendAsync("OnboardingBuildCompleted", result);
        return result;
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        var status = await presence.DisconnectAsync(Context.ConnectionId);
        if (status is not null)
        {
            await Clients.Group(PresenceGroup(status.UserID)).SendAsync("WordCompanionPresenceChanged", status);
            var failedScans = await scanStore.FailRunningForUserAsync(
                status.UserID,
                "The Word Companion disconnected before the scan finished. Reopen Word and try again.");
            foreach (var failedScan in failedScans)
            {
                await Clients.Group(PresenceGroup(status.UserID)).SendAsync("OnboardingScanFailed", failedScan);
            }
        }

        await base.OnDisconnectedAsync(exception);
    }

    private int RequireUserId()
        => TryGetUserId(out var userId) ? userId : throw new HubException("Sign in to use the Word Companion.");

    private bool TryGetUserId(out int userId)
        => int.TryParse(Context.User?.FindFirstValue(ClaimTypes.NameIdentifier), out userId);

    private static string PresenceGroup(int userId) => $"word-companion-presence:{userId}";

    private static bool IsMicrosoftWordState(UserOnboardingState state)
        => string.Equals(state.WritingSoftware, WritingSoftwareValues.MicrosoftWord, StringComparison.Ordinal)
            || state.UsesWordCompanion == true;

    private static ManuscriptScanPreview NormalisePreviewContext(ManuscriptScanPreview preview, int userId, UserOnboardingState state)
        => new()
        {
            PreviewID = preview.PreviewID,
            UserID = preview.UserID == 0 ? userId : preview.UserID,
            OnboardingID = preview.OnboardingID == 0 ? state.UserOnboardingStateID : preview.OnboardingID,
            ProjectID = preview.ProjectID == 0 && state.ProjectID.HasValue ? state.ProjectID.Value : preview.ProjectID,
            BookID = preview.BookID == 0 && state.BookID.HasValue ? state.BookID.Value : preview.BookID,
            Source = preview.Source,
            DocumentTitle = preview.DocumentTitle,
            CompanionDocumentIdentifier = preview.CompanionDocumentIdentifier,
            TotalWordCount = preview.TotalWordCount,
            ChapterCount = preview.ChapterCount,
            SceneCount = preview.SceneCount,
            CharacterCandidateCount = preview.CharacterCandidateCount,
            CreatedUtc = preview.CreatedUtc,
            Chapters = preview.Chapters,
            Scenes = preview.Scenes,
            CharacterCandidates = preview.CharacterCandidates
        };
}
