using System.Threading;
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker;
using ClaudeDo.Worker.Agents;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Findings;
using ClaudeDo.Worker.Git;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Logging;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Tickets;
using ClaudeDo.Worker.Online;
using ClaudeDo.Worker.Online.Interfaces;
using ClaudeDo.Worker.Prime;
using ClaudeDo.Worker.Refine;
using ClaudeDo.Worker.Report;
using ClaudeDo.Worker.Report.Interfaces;
using ClaudeDo.Worker.Skills;
using ClaudeDo.Worker.Usage;
using ClaudeDo.Worker.Usage.Interfaces;
using ClaudeDo.Worker.Usage.TokenTracker;
using ClaudeDo.Worker.Usage.TokenTracker.Interfaces;
using ClaudeDo.Worker.Worktrees;
using Microsoft.EntityFrameworkCore;
using Serilog;

// Single-instance per user session. Multiple launch paths exist (logon task,
// app ensure-running, Restart button); a second instance exits cleanly instead
// of fighting over the SignalR port.
var mutex = new Mutex(true, @"Local\ClaudeDoWorker", out var createdNew);
if (!createdNew)
    return; // another instance already owns the port; exit 0

Paths.MigrateLegacyAppDataRoot();

var cfg = WorkerConfig.Load();

var builder = WebApplication.CreateBuilder(args);

var logRoot = cfg.LogRoot;
Directory.CreateDirectory(logRoot);

// In-memory ring + broadcast sink power the footer log strip and the Log Visualizer
// overlay. Created pre-build so Serilog can write to the sink; the SignalR broadcaster
// is attached once the host is built (see below).
var logBuffer = new LogRingBuffer(TimeSpan.FromMinutes(30));
var broadcastSink = new BroadcastLogSink(logBuffer);

builder.Host.UseSerilog((ctx, lc) => lc
    .MinimumLevel.Information()
    .MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning)
    .MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
    .WriteTo.File(
        System.IO.Path.Combine(logRoot, "worker-.log"),
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 7,
        shared: true)
    .WriteTo.Sink(broadcastSink));
builder.Services.AddSingleton(logBuffer);

builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
    opt.UseSqlite($"Data Source={cfg.DbPath}"));

builder.Services.AddSingleton(cfg);
builder.Services.AddSingleton<AttachmentStore>();
// Singleton so the always-on and in-task MCP servers share the same index write lock.
builder.Services.AddSingleton<FindingsStore>();
builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddHostedService<OrphanRecovery>();
builder.Services.AddHostedService<AttachmentOrphanRecovery>();
builder.Services.AddHostedService<PromptFileRecovery>();
builder.Services.AddHostedService<LegacyWorktreeFolderRecovery>();
builder.Services.AddSignalR().AddJsonProtocol(options =>
{
    options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});

// Runner stack.
builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
builder.Services.AddSingleton<HubBroadcaster>();
builder.Services.AddSingleton<GitService>();
builder.Services.AddSingleton<WorktreeManager>();
builder.Services.AddSingleton<ClaudeArgsBuilder>();
builder.Services.AddSingleton<TaskRunTokenRegistry>();
builder.Services.AddSingleton<PendingQuestionRegistry>();
builder.Services.AddSingleton<IRepoCloner, GitRepoCloner>();
builder.Services.AddSingleton<ISessionSkillRegistry, SessionSkillRegistry>();
builder.Services.AddSingleton<ISessionSkillSeeder, SessionSkillSeeder>();
builder.Services.AddSingleton<InteractiveLaunchSpecService>();
builder.Services.AddSingleton<TaskRunner>();
builder.Services.AddSingleton<WorktreeMaintenanceService>();
builder.Services.AddSingleton<TaskResetService>();
builder.Services.AddSingleton<IVerifyCommandRunner, VerifyCommandRunner>();
builder.Services.AddSingleton<TaskMergeService>();
builder.Services.AddSingleton<InteractiveReviewSubmissionService>();
builder.Services.AddSingleton<PlanningAggregator>();
builder.Services.AddSingleton<PlanningMergeOrchestrator>();
builder.Services.AddSingleton<PlanningChainCoordinator>();

// Queue dispatch primitives. QueueWaker holds the wake semaphore; the queue picker
// performs atomic Queued→Running claim. Both injected into the state service so it
// can wake the dispatcher without depending on QueueService directly.
builder.Services.AddSingleton<QueueWaker>();
builder.Services.AddSingleton<IQueueWaker>(sp => sp.GetRequiredService<QueueWaker>());
builder.Services.AddSingleton<IQueuePicker, QueuePicker>();
builder.Services.AddSingleton<RunCancellationRegistry>();

builder.Services.AddSingleton<Func<ITaskStateService>>(sp => () => sp.GetRequiredService<ITaskStateService>());
// PlanningMergeOrchestrator itself depends on ITaskStateService, so TaskStateService can only
// reach it lazily (Func<IActiveMergeState>) — same cycle-breaking shape as the Func above.
builder.Services.AddSingleton<Func<IActiveMergeState>>(sp => () => sp.GetRequiredService<PlanningMergeOrchestrator>());
builder.Services.AddSingleton<BaseDirtyChecker>();
builder.Services.AddSingleton<ITaskStateService>(sp => new TaskStateService(
    sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
    sp.GetRequiredService<HubBroadcaster>(),
    sp.GetRequiredService<IQueueWaker>(),
    sp.GetRequiredService<PlanningChainCoordinator>(),
    sp.GetRequiredService<RunCancellationRegistry>(),
    sp.GetRequiredService<Func<IActiveMergeState>>(),
    sp.GetRequiredService<BaseDirtyChecker>(),
    sp.GetRequiredService<ILogger<TaskStateService>>(),
    sp.GetRequiredService<TicketStatusSync>()));

// Agent file management.
var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents");
Directory.CreateDirectory(agentsDir);
builder.Services.AddSingleton(new AgentFileService(agentsDir));

var defaultAgentsBundleDir = Path.Combine(AppContext.BaseDirectory, "DefaultAgents");
builder.Services.AddSingleton(sp => new DefaultAgentSeeder(
    defaultAgentsBundleDir,
    agentsDir,
    sp.GetService<Microsoft.Extensions.Logging.ILogger<DefaultAgentSeeder>>()));

// Override slot owns RunNow / ContinueTask. Queue slot is the BackgroundService.
builder.Services.AddSingleton<OverrideSlotService>();

builder.Services.AddSingleton<IClaudeHistoryReader>(_ =>
    new ClaudeHistoryReader(Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".claude", "projects")));
builder.Services.AddSingleton<WeekReportService>();

// Usage
builder.Services.AddSingleton<ITranscriptUsageReader, TranscriptUsageReader>();

// TokenTracker — the analytics backend. Optional at runtime: every consumer degrades when the
// CLI is missing, so nothing here is a startup requirement.
builder.Services.AddSingleton<ITokenTrackerClient, TokenTrackerClient>();
builder.Services.AddSingleton<TokenTrackerState>();
builder.Services.AddSingleton<TokenTrackerService>();

// Prime Claude
builder.Services.AddSingleton<IPrimeClock, PrimeClock>();
builder.Services.AddSingleton<PrimeScheduleSignal>();
builder.Services.AddSingleton<IPrimeRunner, PrimeRunner>();
builder.Services.AddSingleton(PrimeSchedulerOptions.Default);
builder.Services.AddSingleton<IPrimeBroadcaster>(sp => sp.GetRequiredService<HubBroadcaster>());
builder.Services.AddHostedService<PrimeScheduler>();

// Refine
builder.Services.AddSingleton<RefineRunner>();
builder.Services.AddSingleton<IRefineBroadcaster>(sp => sp.GetRequiredService<HubBroadcaster>());

// "Continue on session limit reset" toggle — depends on UsageState, registered below.
builder.Services.AddSingleton<UsageLimitAutoContinueCoordinator>();

// QueueService: singleton + hosted service (same instance).
builder.Services.AddSingleton<QueueService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<QueueService>());

// Planning session services.
var planningSessionsDir = Path.Combine(Paths.AppDataRoot(), "planning-sessions");
builder.Services.AddSingleton(sp =>
    new PlanningSessionManager(
        sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
        sp.GetRequiredService<GitService>(),
        cfg,
        sp.GetRequiredService<ITaskStateService>(),
        sp.GetRequiredService<PlanningChainCoordinator>(),
        planningSessionsDir));
builder.Services.AddHostedService(sp => new PlanningLineageRecovery(
    sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
    planningSessionsDir,
    sp.GetRequiredService<HubBroadcaster>(),
    sp.GetRequiredService<ILogger<PlanningLineageRecovery>>()));
builder.Services.AddSingleton<ITerminalLauncher>(sp =>
    new WindowsTerminalLauncher("wt.exe", cfg.ClaudeBin));
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<PlanningMcpContextAccessor>();
builder.Services.AddScoped<TaskRunMcpContextAccessor>();
builder.Services.AddScoped<TaskRunMcpService>();
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
    sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<PlanningMcpService>();
builder.Services.AddScoped<FindingsStoreLocator>();
builder.Services.AddScoped<TaskRunFindingsMcpTools>();
builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = true)
    .WithTools<PlanningMcpService>()
    .WithTools<TaskRunMcpService>()
    .WithTools<TaskRunFindingsMcpTools>();

// OnlineInboxConfig and the refresh-token store are always registered so hub methods work
// even when sync is disabled. The sync stack (api client, auth, hosted service) is
// only registered when enabled.
builder.Services.AddSingleton(cfg.OnlineInbox);
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
builder.Services.AddSingleton(new OnlineRefreshTokenStore(DpapiTokenStore.InAppData("online-inbox.token")));
builder.Services.AddSingleton(new TicketPatStore(DpapiTokenStore.InAppData("ticket.pat")));
#pragma warning restore CA1416
builder.Services.AddSingleton<TicketSystemConfig>();
builder.Services.AddHttpClient("tickets");
builder.Services.AddSingleton<TicketClientFactory>();
builder.Services.AddSingleton<TicketStatusSync>();
builder.Services.AddSingleton<TicketImportService>();

if (cfg.OnlineInbox.Enabled)
{
    OnlineInboxApiClient.ValidateBaseUrl(cfg.OnlineInbox.ApiBaseUrl);
    builder.Services.AddHttpClient();
#pragma warning disable CA1416
    builder.Services.AddSingleton<IOnlineAuthProvider, ZitadelAuthProvider>();
#pragma warning restore CA1416
    builder.Services.AddHttpClient<IOnlineInboxApi, OnlineInboxApiClient>(client =>
    {
        client.BaseAddress = new Uri(cfg.OnlineInbox.ApiBaseUrl.TrimEnd('/') + "/");
    });
    builder.Services.AddHostedService<OnlineSyncService>();
}

// OAuth usage monitor: reads the access token Claude Code keeps fresh in
// ~/.claude/.credentials.json and polls Anthropic's usage endpoint.
builder.Services.AddSingleton<UsageState>();
builder.Services.AddHttpClient<IUsageClient, ClaudeOAuthUsageClient>(client =>
{
    client.Timeout = TimeSpan.FromSeconds(5);
});
builder.Services.AddSingleton<IUsageGate, UsageGate>();
builder.Services.AddSingleton<UsageSnapshotBuilder>();
builder.Services.AddSingleton<IRunningTaskProbe, RunningTaskProbe>();
// Singleton + hosted service (same instance) so WorkerHub.RefreshUsage can drive a manual poll.
builder.Services.AddSingleton<UsageMonitorService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<UsageMonitorService>());

// Loopback-only bind. Firewall is irrelevant for 127.0.0.1.
builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}");

var app = builder.Build();

// Now that the hub context exists, let the broadcast sink push Warn/Error to the footer.
var logBroadcaster = app.Services.GetRequiredService<HubBroadcaster>();
broadcastSink.Attach((message, level, ts) => logBroadcaster.WorkerLog(message, level, ts));

using (var scope = app.Services.CreateScope())
{
    ClaudeDoDbContext.MigrateAndConfigure(
        scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>());
}

try
{
    var seeder = app.Services.GetRequiredService<DefaultAgentSeeder>();
    var seedResult = await seeder.SeedMissingAsync();
    app.Logger.LogInformation(
        "Default agents seeded: {Copied} copied, {Skipped} already present",
        seedResult.Copied, seedResult.Skipped);
}
catch (Exception ex)
{
    app.Logger.LogWarning(ex, "Default agent seeding failed");
}

app.UseMiddleware<PlanningTokenAuthMiddleware>();
app.MapHub<WorkerHub>("/hub");
app.MapMcp("/mcp");

// Claude CLI preflight: fail fast if the configured binary is unreachable or non-zero.
// Skippable via CLAUDEDO_SKIP_CLI_PREFLIGHT=1 for environments without the CLI (e.g. tests).
if (Environment.GetEnvironmentVariable("CLAUDEDO_SKIP_CLI_PREFLIGHT") != "1")
{
    var preflight = await ClaudeCliPreflight.CheckAsync(cfg.ClaudeBin);
    if (!preflight.Ok)
    {
        app.Logger.LogCritical(
            "Claude CLI preflight failed (bin: '{Bin}', exit: {Exit}): {Error}. " +
            "Fix `claude_bin` in worker.config.json or set CLAUDEDO_SKIP_CLI_PREFLIGHT=1 to bypass.",
            cfg.ClaudeBin, preflight.ExitCode, preflight.Error);
        Environment.Exit(1);
    }
    app.Logger.LogInformation("Claude CLI preflight OK: {Version}", preflight.Version);
}

app.Logger.LogInformation("ClaudeDo.Worker listening on http://127.0.0.1:{Port} (db: {Db})",
    cfg.SignalRPort, cfg.DbPath);

// Build the external MCP endpoint as a separate WebApplication on its own port.
// Rationale: ModelContextProtocol.AspNetCore registers one server per DI container,
// so we need a second app to expose a different tool set under different auth.
// Shared singletons (QueueService, HubBroadcaster, WorkerConfig, db factory) are
// injected by instance so both apps operate on the same runtime state.
WebApplication? externalApp = null;
if (cfg.ExternalMcpPort > 0)
{
    var externalBuilder = WebApplication.CreateBuilder();
    // Hand the second container the SAME singleton instances, so both apps operate on one
    // runtime state instead of two parallel copies of the queue/git/merge stack.
    void Share<T>() where T : class =>
        externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<T>());

    externalBuilder.Services.AddSingleton(cfg);
    Share<HubBroadcaster>();
    Share<QueueService>();
    Share<OverrideSlotService>();
    Share<IDbContextFactory<ClaudeDoDbContext>>();
    Share<ITaskStateService>();
    Share<IQueueWaker>();
    Share<WorktreeManager>();
    Share<AgentFileService>();
    Share<TaskResetService>();
    Share<GitService>();
    Share<BaseDirtyChecker>();
    Share<WorktreeMaintenanceService>();
    Share<TaskMergeService>();
    Share<PlanningMergeOrchestrator>();
    Share<InteractiveReviewSubmissionService>();
    Share<AttachmentStore>();
    Share<FindingsStore>();

    externalBuilder.Services.AddScoped<ClaudeDoDbContext>(sp =>
        sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
    externalBuilder.Services.AddScoped<TaskRepository>();
    externalBuilder.Services.AddScoped<ListRepository>();
    externalBuilder.Services.AddScoped<TaskRunRepository>();
    externalBuilder.Services.AddScoped<TaskAttachmentRepository>();
    externalBuilder.Services.AddSingleton<HandoffRoundTracker>();
    externalBuilder.Services.AddScoped<FindingsStoreLocator>();
    externalBuilder.Services.AddScoped<ExternalMcpService>();
    externalBuilder.Services.AddScoped<BatchMcpTools>();
    externalBuilder.Services.AddScoped<ListMcpTools>();
    externalBuilder.Services.AddScoped<ConfigMcpTools>();
    externalBuilder.Services.AddScoped<RunHistoryMcpTools>();
    externalBuilder.Services.AddScoped<AgentMcpTools>();
    externalBuilder.Services.AddScoped<LifecycleMcpTools>();
    externalBuilder.Services.AddScoped<HandoffMcpTools>();
    externalBuilder.Services.AddScoped<AppSettingsMcpTools>();
    externalBuilder.Services.AddScoped<TaskWaitMcpTools>();
    externalBuilder.Services.AddScoped<QueueStateMcpTools>();
    externalBuilder.Services.AddScoped<AttachmentMcpTools>();
    externalBuilder.Services.AddScoped<DailyNoteRepository>();
    externalBuilder.Services.AddScoped<FindingsMcpTools>();
    externalBuilder.Services.AddScoped<NotesMcpTools>();
    externalBuilder.Services.AddMcpServer()
        .WithHttpTransport(o => o.Stateless = true)
        .WithRequestFilters(f => f.AddCallToolFilter(ExternalMcpExceptionFilter.Wrap))
        .WithTools<ExternalMcpService>()
        .WithTools<BatchMcpTools>()
        .WithTools<ListMcpTools>()
        .WithTools<ConfigMcpTools>()
        .WithTools<RunHistoryMcpTools>()
        .WithTools<AgentMcpTools>()
        .WithTools<LifecycleMcpTools>()
        .WithTools<HandoffMcpTools>()
        .WithTools<AppSettingsMcpTools>()
        .WithTools<TaskWaitMcpTools>()
        .WithTools<QueueStateMcpTools>()
        .WithTools<AttachmentMcpTools>()
        .WithTools<FindingsMcpTools>()
        .WithTools<NotesMcpTools>();
    externalBuilder.WebHost.UseUrls($"http://127.0.0.1:{cfg.ExternalMcpPort}");

    externalApp = externalBuilder.Build();
    externalApp.UseMiddleware<ExternalMcpAuthMiddleware>();
    externalApp.MapMcp("/mcp");

    externalApp.Logger.LogInformation(
        "ClaudeDo.Worker external MCP listening on http://127.0.0.1:{Port} (auth: {Auth})",
        cfg.ExternalMcpPort,
        string.IsNullOrEmpty(cfg.ExternalMcpApiKey) ? "loopback-only" : "X-ClaudeDo-Key");
}

if (externalApp is null)
{
    await app.RunAsync();
}
else
{
    await Task.WhenAll(app.RunAsync(), externalApp.RunAsync());
}

GC.KeepAlive(mutex);
