using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Agents;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.External;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.State;
using ClaudeDo.Worker.Prime;
using ClaudeDo.Worker.Worktrees;
using Microsoft.EntityFrameworkCore;

var cfg = WorkerConfig.Load();

var builder = WebApplication.CreateBuilder(args);

// When launched by the Windows SCM, speak the Service Control Protocol so SCM
// doesn't think we crashed (~30s timeout). No-op when running interactively.
builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");

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

builder.Services.AddSingleton(cfg);
builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddHostedService<OrphanRecovery>();
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<TaskRunner>();
builder.Services.AddSingleton<WorktreeMaintenanceService>();
builder.Services.AddSingleton<TaskResetService>();
builder.Services.AddSingleton<TaskMergeService>();
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<Func<ITaskStateService>>(sp => () => sp.GetRequiredService<ITaskStateService>());
builder.Services.AddSingleton<ITaskStateService>(sp => new TaskStateService(
    sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
    sp.GetRequiredService<HubBroadcaster>(),
    sp.GetRequiredService<IQueueWaker>(),
    sp.GetRequiredService<PlanningChainCoordinator>(),
    sp.GetRequiredService<ILogger<TaskStateService>>()));

// 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>();

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

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

// Planning session services.
var planningSessionsDir = Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
    ".todo-app", "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<ILogger<PlanningLineageRecovery>>()));
builder.Services.AddSingleton<IPlanningTerminalLauncher>(sp =>
    new WindowsTerminalPlanningLauncher("wt.exe", cfg.ClaudeBin));
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<PlanningMcpContextAccessor>();
builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
    sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<PlanningMcpService>();
builder.Services.AddMcpServer()
    .WithHttpTransport()
    .WithTools<PlanningMcpService>();

// 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();

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();
    externalBuilder.Services.AddSingleton(cfg);
    externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<HubBroadcaster>());
    externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<QueueService>());
    externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<OverrideSlotService>());
    externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>());
    externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<ITaskStateService>());
    externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<IQueueWaker>());
    externalBuilder.Services.AddScoped<ClaudeDoDbContext>(sp =>
        sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
    externalBuilder.Services.AddScoped<TaskRepository>();
    externalBuilder.Services.AddScoped<ListRepository>();
    externalBuilder.Services.AddScoped<ExternalMcpService>();
    externalBuilder.Services.AddMcpServer()
        .WithHttpTransport()
        .WithTools<ExternalMcpService>();
    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());
}
