using System.Security.Cryptography;
using System.Text.Json;
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.State;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Planning;

public sealed class PlanningSessionManager
{
    private string McpServerUrl => $"http://127.0.0.1:{_cfg.SignalRPort}/mcp";

    private readonly IDbContextFactory<ClaudeDoDbContext>? _factory;
    private readonly TaskRepository? _tasksOverride;
    private readonly ListRepository? _listsOverride;
    private readonly AppSettingsRepository? _settingsOverride;
    private readonly GitService _git;
    private readonly WorkerConfig _cfg;
    private readonly string _rootDirectory;
    private readonly string _projectsRoot;
    private readonly ITaskStateService? _state;
    private readonly PlanningChainCoordinator? _chain;

    // DI constructor.
    public PlanningSessionManager(
        IDbContextFactory<ClaudeDoDbContext> factory,
        GitService git,
        WorkerConfig cfg,
        ITaskStateService state,
        PlanningChainCoordinator chain,
        string rootDirectory,
        string? projectsRoot = null)
    {
        _factory = factory;
        _git = git;
        _cfg = cfg;
        _state = state;
        _chain = chain;
        _rootDirectory = rootDirectory;
        _projectsRoot = projectsRoot ?? Paths.Expand("~/.claude/projects");
    }

    // Test constructor.
    public PlanningSessionManager(
        TaskRepository tasks,
        ListRepository lists,
        AppSettingsRepository settings,
        GitService git,
        WorkerConfig cfg,
        string rootDirectory,
        ITaskStateService? state = null,
        PlanningChainCoordinator? chain = null,
        string? projectsRoot = null)
    {
        _tasksOverride = tasks;
        _listsOverride = lists;
        _settingsOverride = settings;
        _git = git;
        _cfg = cfg;
        _state = state;
        _chain = chain;
        _rootDirectory = rootDirectory;
        _projectsRoot = projectsRoot ?? Paths.Expand("~/.claude/projects");
    }

    private (TaskRepository tasks, ListRepository lists, AppSettingsRepository settings, ClaudeDoDbContext? ctx) CreateRepos()
    {
        if (_tasksOverride is not null)
            return (_tasksOverride, _listsOverride!, _settingsOverride!, null);
        var ctx = _factory!.CreateDbContext();
        return (new TaskRepository(ctx), new ListRepository(ctx), new AppSettingsRepository(ctx), ctx);
    }

    public async Task<PlanningSessionStartContext> StartAsync(string taskId, CancellationToken ct)
    {
        var (tasks, lists, settings, ctx) = CreateRepos();
        await using var _ = ctx;

        var task = await tasks.GetByIdAsync(taskId, ct)
            ?? throw new InvalidOperationException($"Task {taskId} not found.");
        if (task.ParentTaskId is not null)
            throw new InvalidOperationException("Cannot start a planning session on a child task.");
        if (task.Status != TaskStatus.Idle || task.PlanningPhase != PlanningPhase.None)
            throw new InvalidOperationException(
                $"Task is in status {task.Status}/{task.PlanningPhase}; only Idle+None can start planning.");

        var list = await lists.GetByIdAsync(task.ListId, ct)
            ?? throw new InvalidOperationException($"List {task.ListId} not found.");
        var listWorkingDir = list.WorkingDir
            ?? throw new InvalidOperationException($"List {task.ListId} has no working directory configured.");

        if (!await _git.IsGitRepoAsync(listWorkingDir, ct))
            throw new InvalidOperationException($"Working directory is not a git repository: {listWorkingDir}");

        // A fresh `git init` repo has no HEAD commit to base the planning worktree on.
        await _git.EnsureHeadCommitAsync(listWorkingDir, ct);

        var appSettings = await settings.GetAsync(ct);
        var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
        var branchName = BranchNameFor(taskId);
        var baseCommit = await _git.RevParseHeadAsync(listWorkingDir, ct);

        Directory.CreateDirectory(Path.GetDirectoryName(worktreePath)!);
        try
        {
            await _git.WorktreeAddAsync(listWorkingDir, branchName, worktreePath, baseCommit, ct);
        }
        catch (InvalidOperationException ex) when (ex.Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
        {
            // Self-heal: remove phantom worktrees, prune, delete branch, retry once.
            var stalePaths = await _git.ListWorktreePathsForBranchAsync(listWorkingDir, branchName, ct);
            foreach (var stale in stalePaths)
            {
                try { await _git.WorktreeRemoveAsync(listWorkingDir, stale, force: true, ct); } catch { }
            }
            try { await _git.WorktreePruneAsync(listWorkingDir, ct); } catch { }
            try { await _git.BranchDeleteAsync(listWorkingDir, branchName, force: true, ct); } catch { }
            await _git.WorktreeAddAsync(listWorkingDir, branchName, worktreePath, baseCommit, ct);
        }

        // Write .mcp.json and .claude/settings.local.json into the worktree.
        var mcpPath = Path.Combine(worktreePath, ".mcp.json");
        await File.WriteAllTextAsync(mcpPath, BuildMcpConfigJson(), ct);

        var claudeDir = Path.Combine(worktreePath, ".claude");
        Directory.CreateDirectory(claudeDir);
        await File.WriteAllTextAsync(Path.Combine(claudeDir, "settings.local.json"), SettingsLocalJson, ct);

        // Session dir + token + prompt files.
        var token = GenerateToken();
        if (_state is not null)
        {
            var startResult = await _state.StartPlanningAsync(taskId, ct);
            if (!startResult.Ok)
                throw new InvalidOperationException(startResult.Reason ?? "Failed to transition task to Planning.");
            await tasks.SetPlanningSessionTokenAsync(taskId, token, ct);
        }
        else
        {
            // Test fallback when no state-service is provided.
            if (await tasks.SetPlanningStartedAsync(taskId, token, ct) is null)
                throw new InvalidOperationException("Failed to transition task to Planning.");
        }

        var sessionDir = Path.Combine(_rootDirectory, taskId);
        Directory.CreateDirectory(sessionDir);

        var files = new PlanningSessionFiles(
            sessionDir,
            Path.Combine(sessionDir, "system-prompt.md"),
            Path.Combine(sessionDir, "initial-prompt.txt"));

        await WriteTokenFileAsync(TokenFilePathFor(sessionDir), token, ct);
        await File.WriteAllTextAsync(files.SystemPromptPath, BuildSystemPrompt(), ct);
        await File.WriteAllTextAsync(files.InitialPromptPath, BuildInitialPrompt(task), ct);

        return new PlanningSessionStartContext(
            ParentTaskId: taskId,
            WorkingDir: worktreePath,
            Token: token,
            WorktreePath: worktreePath,
            BranchName: branchName,
            Files: files);
    }

    public async Task<int> FinalizeAsync(string taskId, bool queueAgentTasks, CancellationToken ct)
    {
        var (tasks, lists, settings, ctx) = CreateRepos();
        await using var __ = ctx;

        if (_state is null || _chain is null)
            throw new InvalidOperationException(
                "PlanningSessionManager.FinalizeAsync requires ITaskStateService and PlanningChainCoordinator.");

        var finalizeResult = await _state.FinalizePlanningAsync(taskId, ct);
        if (!finalizeResult.Ok)
            throw new InvalidOperationException(
                finalizeResult.Reason ?? $"Could not finalize planning for task {taskId}.");

        // Establish the blocked-by chain but leave children Idle; queueing is a
        // deliberate user action ("Queue plan"), never an automatic finalize step.
        // queueAgentTasks is accepted for compatibility but no longer auto-queues.
        var children = await tasks.GetChildrenAsync(taskId, ct);
        int count = children.Count;
        if (children.Count > 0)
            count = await _chain.SetupChainAsync(taskId, enqueue: false, ct);

        // Best-effort cleanup — don't block finalization on git state.
        await TryCleanupWorktreeAsync(taskId, lists, settings, ct);

        var sessionDir = Path.Combine(_rootDirectory, taskId);
        if (Directory.Exists(sessionDir))
        {
            try { Directory.Delete(sessionDir, recursive: true); } catch { }
        }

        return count;
    }

    public async Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct)
    {
        var (tasks, _, settings, ctx) = CreateRepos();
        await using var __ = ctx;
        var children = await tasks.GetChildrenAsync(taskId, ct);
        return children.Count(c => c.Status == TaskStatus.Idle);
    }

    public async Task<DiscardPlanningOutcome> DiscardAsync(
        string taskId,
        bool dequeueQueuedChildren,
        CancellationToken ct)
    {
        var (tasks, lists, settings, ctx) = CreateRepos();
        await using var __ = ctx;

        var outcome = await tasks.DiscardPlanningAsync(taskId, dequeueQueuedChildren, ct);
        if (outcome.Result != DiscardPlanningResult.Discarded)
            return outcome;

        await TryCleanupWorktreeAsync(taskId, lists, settings, ct);

        var sessionDir = Path.Combine(_rootDirectory, taskId);
        if (Directory.Exists(sessionDir))
        {
            try { Directory.Delete(sessionDir, recursive: true); } catch { }
        }

        return outcome;
    }

    public async Task<PlanningSessionResumeContext> ResumeAsync(string taskId, CancellationToken ct)
    {
        var (tasks, lists, settings, ctx) = CreateRepos();
        await using var _ = ctx;

        var task = await tasks.GetByIdAsync(taskId, ct)
            ?? throw new InvalidOperationException($"Task {taskId} not found.");
        if (task.PlanningPhase != PlanningPhase.Active)
            throw new InvalidOperationException(
                $"Task planning phase is {task.PlanningPhase}; resume requires Active planning.");

        var sessionDir = Path.Combine(_rootDirectory, taskId);
        if (!Directory.Exists(sessionDir))
            throw new InvalidOperationException($"Session directory missing: {sessionDir}");

        var list = await lists.GetByIdAsync(task.ListId, ct)
            ?? throw new InvalidOperationException($"List {task.ListId} not found.");
        var listWorkingDir = list.WorkingDir
            ?? throw new InvalidOperationException($"List {task.ListId} has no working directory configured.");

        var appSettings = await settings.GetAsync(ct);
        var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
        if (!Directory.Exists(worktreePath))
            throw new InvalidOperationException($"Planning worktree missing — cannot resume: {worktreePath}");

        // The planning session runs in an interactive TUI, which never reports its session id
        // back to us — so nothing ever wrote planning_session_id. Recover it from the transcript
        // Claude Code leaves on disk for the planning worktree, and persist it so the next
        // resume is a plain DB read.
        var sessionId = task.PlanningSessionId;
        if (string.IsNullOrEmpty(sessionId))
        {
            sessionId = PlanningTranscriptLocator.FindSessionId(worktreePath, _projectsRoot)
                ?? throw new InvalidOperationException(
                    "No Claude session transcript found for this planning worktree; cannot resume. " +
                    "Finalize or discard the planning session instead.");
            await tasks.UpdatePlanningSessionIdAsync(taskId, sessionId, ct);
        }

        var token = await ReadTokenFileAsync(TokenFilePathFor(sessionDir), ct);

        return new PlanningSessionResumeContext(
            ParentTaskId: taskId,
            WorkingDir: worktreePath,
            ClaudeSessionId: sessionId,
            Token: token,
            WorktreePath: worktreePath);
    }

    private async Task TryCleanupWorktreeAsync(
        string taskId,
        ListRepository lists,
        AppSettingsRepository settings,
        CancellationToken ct)
    {
        try
        {
            var (tasks, _, _, ctx2) = CreateRepos();
            await using var __ = ctx2;

            var task = await tasks.GetByIdAsync(taskId, ct);
            if (task is null) return;

            var list = await lists.GetByIdAsync(task.ListId, ct);
            var listWorkingDir = list?.WorkingDir;
            if (string.IsNullOrEmpty(listWorkingDir) || !Directory.Exists(listWorkingDir)) return;

            var appSettings = await settings.GetAsync(ct);
            var worktreePath = WorktreePathFor(taskId, appSettings.WorktreeStrategy, appSettings.CentralWorktreeRoot, listWorkingDir);
            var branchName = BranchNameFor(taskId);

            if (Directory.Exists(worktreePath))
            {
                try { await _git.WorktreeRemoveAsync(listWorkingDir, worktreePath, force: true, ct); }
                catch { /* best effort */ }
            }
            try { await _git.BranchDeleteAsync(listWorkingDir, branchName, force: true, ct); } catch { }
        }
        catch { /* best effort — never block finalize/discard */ }
    }

    private static string GenerateToken()
    {
        var bytes = RandomNumberGenerator.GetBytes(32);
        return Convert.ToBase64String(bytes)
            .Replace('+', '-')
            .Replace('/', '_')
            .TrimEnd('=');
    }

    private string BuildMcpConfigJson()
    {
        var payload = new
        {
            mcpServers = new
            {
                claudedo = new
                {
                    type = "http",
                    url = McpServerUrl,
                    headers = new Dictionary<string, string>
                    {
                        ["Authorization"] = "Bearer ${CLAUDEDO_PLANNING_TOKEN}"
                    }
                }
            }
        };
        return JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
    }

    private const string SettingsLocalJson = """
        {
          "enableAllProjectMcpServers": true
        }
        """;

    private static string BuildSystemPrompt() => PromptFiles.ReadOrDefault(PromptKind.Planning);

    private static string BuildInitialPrompt(TaskEntity task) =>
        PromptFiles.Render(PromptKind.PlanningInitial, new Dictionary<string, string>
        {
            ["title"] = task.Title,
            ["description"] = task.Description ?? "",
        });

    private static string BranchNameFor(string taskId) =>
        $"claudedo/planning/{taskId.Replace("-", "")}";

    private string WorktreePathFor(string taskId, string strategy, string? centralRootOverride, string listWorkingDir)
    {
        var centralRoot = !string.IsNullOrWhiteSpace(centralRootOverride)
            ? centralRootOverride!
            : _cfg.CentralWorktreeRoot;

        var raw = strategy.Equals("central", StringComparison.OrdinalIgnoreCase)
            ? Path.Combine(centralRoot, "planning", taskId)
            : Path.Combine(WorktreeRootResolver.ResolveSiblingRoot(listWorkingDir), ".claudedo-worktrees", "planning", taskId);

        var worktreePath = Path.GetFullPath(raw);
        WorktreeRootResolver.EnsureOutsideWorkingDir(worktreePath, listWorkingDir);
        return worktreePath;
    }

    private static string TokenFilePathFor(string sessionDir) =>
        Path.Combine(sessionDir, "token");

    private static async Task WriteTokenFileAsync(string path, string token, CancellationToken ct)
    {
        await File.WriteAllTextAsync(path, token, ct);
        // Best-effort current-user-only ACL on Windows. On non-Windows the inherited
        // perms from the parent dir apply; acceptable because sessionDir is already
        // under the user's home (~/.claudeDo/sessions/).
        if (OperatingSystem.IsWindows())
        {
            try
            {
                var fi = new FileInfo(path);
                var ac = fi.GetAccessControl();
                ac.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
                var me = System.Security.Principal.WindowsIdentity.GetCurrent().User!;
                ac.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule(
                    me,
                    System.Security.AccessControl.FileSystemRights.FullControl,
                    System.Security.AccessControl.AccessControlType.Allow));
                fi.SetAccessControl(ac);
            }
            catch { /* ACL hardening is best-effort */ }
        }
    }

    private static async Task<string> ReadTokenFileAsync(string path, CancellationToken ct)
    {
        if (!File.Exists(path))
            throw new InvalidOperationException($"Token file missing: {path}");
        return (await File.ReadAllTextAsync(path, ct)).Trim();
    }
}

/// <summary>
/// Recovers the Claude session id of an interactive session from the transcript Claude Code
/// writes to <c>~/.claude/projects/&lt;encoded cwd&gt;/&lt;sessionId&gt;.jsonl</c>. A planning
/// worktree is used by exactly one session, so the newest transcript in that folder is it.
/// </summary>
/// <remarks>
/// The folder-name encoding is undocumented CLI behaviour (every non-alphanumeric character in
/// the absolute path becomes '-'). If it ever changes, <see cref="FindSessionId"/> returns null
/// and the caller reports "cannot resume" instead of resuming the wrong session.
/// </remarks>
public static class PlanningTranscriptLocator
{
    public static string? FindSessionId(string worktreePath, string projectsRoot)
    {
        var dir = Path.Combine(projectsRoot, EncodeCwd(worktreePath));
        if (!Directory.Exists(dir)) return null;

        var newest = new DirectoryInfo(dir)
            .EnumerateFiles("*.jsonl")
            .OrderByDescending(f => f.LastWriteTimeUtc)
            .FirstOrDefault();

        return newest is null ? null : Path.GetFileNameWithoutExtension(newest.Name);
    }

    private static string EncodeCwd(string path)
    {
        var full = Path.GetFullPath(path);
        return string.Create(full.Length, full, static (span, src) =>
        {
            for (var i = 0; i < src.Length; i++)
                span[i] = char.IsAsciiLetterOrDigit(src[i]) ? src[i] : '-';
        });
    }
}