import { serve } from "bun";
import { createGitHubTokenProvider, verifyGitHubAuth } from "@/github/auth.js";
import { GitHubClientImpl, SandboxGitHubClient } from "@/github/client.js";
import { JiraClientImpl, SandboxJiraClient } from "@/jira/client.js";
import { CursorCliClient, CursorCloudClient, ClaudeCliClient } from "@/agent/client.js";
import type { AgentRunner } from "@/types/index.js";
import { RepoRegistry } from "@/repo/registry.js";
import { RepoSelector } from "@/repo/selector.js";
import { MemoryQueue } from "@/queue/memory-queue.js";
import { FixPipeline } from "@/pipeline/fix-pipeline.js";
import { ReviewTicketPipeline } from "@/pipeline/review-ticket-pipeline.js";
import { PrCommentPipeline } from "@/pipeline/pr-comment-pipeline.js";
import { PrMentionPipeline } from "@/pipeline/pr-mention-pipeline.js";
import { JiraPoller } from "@/poller/jira-poller.js";
import { JiraTicketReviewPoller } from "@/poller/jira-ticket-review-poller.js";
import { PrCommentPoller } from "@/poller/pr-comment-poller.js";
import { PrMentionPoller } from "@/poller/pr-mention-poller.js";
import { DailyReportScheduler } from "@/scheduler/daily-report.js";
import { DailyReportStore } from "@/scheduler/daily-report-store.js";
import { PromptCleanupScheduler } from "@/scheduler/prompt-cleanup.js";
import { SlackMentionPipeline } from "@/pipeline/slack-mention-pipeline.js";
import { SlackMentionPoller } from "@/poller/slack-mention-poller.js";
import { createApp } from "@/server/app.js";
import { SlackNotifier } from "@/slack/client.js";
import { configureGitAuth, prewarmRepos } from "@/repo/manager.js";
import { configureCodeArtifactAuth } from "@/utils/codeartifact.js";
import { loadConfig } from "@/utils/config.js";
import { createLogger } from "@/utils/logger.js";
import { isFfmpegAvailable } from "@/utils/video-frames.js";
import { DatadogLogsClient } from "@/datadog/client.js";
import {
  BOT_VERSION,
  BOT_ENV,
  DEV,
  INSTANCE_ID,
  OBSERVABILITY,
  SHUTDOWN,
} from "@/core/constants.js";
import { createMetrics } from "@/metrics/index.js";
import { METRIC } from "@/metrics/names.js";
import { AuthChecker } from "@/health/auth-checker.js";
import type { AuthProbe } from "@/health/auth-checker.js";

async function main(): Promise<void> {
  const config = loadConfig();

  const logger = createLogger(config.logLevel);
  logger.info(
    { version: BOT_VERSION, instanceId: INSTANCE_ID, env: BOT_ENV, isDev: config.isDev },
    `hu-developer-bot v${BOT_VERSION} (${INSTANCE_ID}) [${BOT_ENV}] starting`,
  );
  if (config.isDev) {
    logger.warn(
      { devChannel: config.slackDevChannelId },
      "DEV mode active: pollers force-disabled, every Slack post redirected with [DEV] prefix. Set NODE_ENV=production to disable.",
    );
  }

  const metrics = createMetrics(
    {
      metricsEnabled: config.metricsEnabled,
      ddAgentHost: config.ddAgentHost,
      ddDogstatsdPort: config.ddDogstatsdPort,
      ddEnv: config.ddEnv,
      provider: config.agentProvider,
    },
    logger.child({ module: "metrics" }),
  );

  const getGitHubToken = createGitHubTokenProvider(config);
  await verifyGitHubAuth(getGitHubToken, logger.child({ module: "github-auth" }));
  await configureGitAuth(getGitHubToken, logger.child({ module: "git" }));

  let stopCodeArtifactRefresh: (() => void) | undefined;
  try {
    stopCodeArtifactRefresh = await configureCodeArtifactAuth(
      {
        domain: config.codeartifactDomain,
        domainOwner: config.codeartifactDomainOwner,
        region: config.codeartifactRegion,
      },
      logger.child({ module: "codeartifact" }),
    );
  } catch (err) {
    logger.warn(
      { err },
      "CodeArtifact auth failed; repos needing @humand packages may fail install",
    );
  }

  if (config.videoAttachmentsEnabled) {
    await isFfmpegAvailable(
      config.ffmpegPath,
      config.ffprobePath,
      logger.child({ module: "video-frames" }),
    );
  } else {
    logger.info("Video attachments disabled via VIDEO_ATTACHMENTS_ENABLED=false");
  }

  const repoRegistry = new RepoRegistry();
  logger.info({ repos: repoRegistry.getAll().map((r) => r.name) }, "Loaded repo registry");

  const JiraClientCls = config.sandboxMode ? SandboxJiraClient : JiraClientImpl;
  const jiraClient = new JiraClientCls(
    config.jiraDomain,
    config.jiraEmail,
    config.jiraApiToken,
    logger.child({ module: "jira" }),
  );

  const agentCli: AgentRunner =
    config.agentProvider === "claude"
      ? new ClaudeCliClient(
          config.anthropicApiKey,
          config.anthropicModel,
          config.anthropicBaseUrl,
          logger.child({ module: "claude-cli" }),
        )
      : new CursorCliClient(
          config.cursorApiKey,
          config.cursorModel,
          logger.child({ module: "cursor-cli" }),
        );

  logger.info(
    {
      provider: config.agentProvider,
      model: config.agentProvider === "claude" ? config.anthropicModel : config.cursorModel,
    },
    "agent_provider_selected",
  );

  const cursorCloudClient = new CursorCloudClient(
    config.cursorApiKey,
    config.cursorModel,
    logger.child({ module: "cursor-cloud" }),
  );

  const repoSelector = new RepoSelector(repoRegistry, logger.child({ module: "selector" }));

  const slackNotifier =
    config.slackBotToken && config.slackChannelId
      ? new SlackNotifier(
          config.slackBotToken,
          config.slackChannelId,
          config.jiraEmail,
          logger.child({ module: "slack" }),
          config.sandboxMode,
          config.isDev ? { channelId: config.slackDevChannelId, prefix: DEV.PREFIX } : undefined,
        )
      : undefined;

  if (
    (config.slackBotToken && !config.slackChannelId) ||
    (!config.slackBotToken && config.slackChannelId)
  ) {
    logger.warn("Slack notifications disabled: set both SLACK_BOT_TOKEN and SLACK_CHANNEL_ID");
  }

  const queue = new MemoryQueue(logger.child({ module: "queue" }), {
    metrics,
    onHighLoad:
      slackNotifier && config.slackAlertChannelId
        ? async (state) => {
            await slackNotifier.notifyHighLoad(
              state.currentJobKey,
              state.currentJobKind,
              state.pendingCount,
              state.pendingJobKeys,
              config.slackAlertChannelId!,
            );
          }
        : undefined,
  });

  const dailyReportStore =
    config.dynamodbTableName.length > 0
      ? new DailyReportStore(config.dynamodbTableName)
      : undefined;

  const dailyReportScheduler =
    config.dailyReportEnabled &&
    slackNotifier &&
    (config.slackDailyReportChannelId ?? config.slackChannelId)
      ? new DailyReportScheduler({
          repoRegistry,
          getGitHubToken,
          slackNotifier,
          slackDailyReportChannelId: config.slackDailyReportChannelId ?? config.slackChannelId,
          dailyReportHours: config.dailyReportHours,
          dailyReportTimezone: config.dailyReportTimezone,
          githubBotUser: config.githubBotUser,
          jiraClient,
          jiraDomain: config.jiraDomain,
          squadScope: config.squadScope,
          botEmail: config.jiraEmail,
          logger: logger.child({ module: "daily-report-scheduler" }),
          store: dailyReportStore,
        })
      : undefined;

  if (dailyReportScheduler && !config.isDev) {
    dailyReportScheduler.start();
  } else if (config.isDev && dailyReportScheduler) {
    logger.info("Daily report scheduler force-disabled (dev mode)");
  } else if (config.slackBotToken && !config.dailyReportEnabled) {
    logger.info("Daily report disabled via DAILY_REPORT_ENABLED=false");
  }

  const promptCleanup = new PromptCleanupScheduler(
    config.workdirPath,
    logger.child({ module: "prompt-cleanup" }),
  );
  promptCleanup.start();

  const pipeline = new FixPipeline({
    metrics,
    jiraClient,
    agentCli,
    repoRegistry,
    getGitHubToken,
    workdirPath: config.workdirPath,
    jiraDomain: config.jiraDomain,
    botEmail: config.jiraEmail,
    slackNotifier,
    getPendingCount: () => queue.pendingCount(),
    logger: logger.child({ module: "pipeline" }),
    buildGitHubClient: (token, repoUrl, ghLogger) =>
      config.sandboxMode
        ? new SandboxGitHubClient(token, repoUrl, config.jiraDomain, ghLogger)
        : new GitHubClientImpl(token, repoUrl, config.jiraDomain, ghLogger),
    videoAttachmentsEnabled: config.videoAttachmentsEnabled,
    ffmpegPath: config.ffmpegPath,
    ffprobePath: config.ffprobePath,
  });

  // ReviewTicketPipeline does not receive video config directly: it delegates
  // card-readiness assessment to FixPipeline.evaluateCardReadiness, which
  // already threads videoAttachmentsEnabled / ffmpegPath / ffprobePath via
  // FixPipeline's own deps.
  const reviewTicketPipeline = new ReviewTicketPipeline({
    jiraClient,
    fixPipeline: pipeline,
    jiraDomain: config.jiraDomain,
    slackNotifier,
    getPendingCount: () => queue.pendingCount(),
    logger: logger.child({ module: "review-ticket-pipeline" }),
  });

  const prCommentPipeline = new PrCommentPipeline({
    metrics,
    agentCli,
    repoRegistry,
    getGitHubToken,
    workdirPath: config.workdirPath,
    slackNotifier,
    slackPrCommentChannelId: config.slackPrCommentChannelId,
    getPendingCount: () => queue.pendingCount(),
    logger: logger.child({ module: "pr-comment-pipeline" }),
  });

  // Pipeline is constructed unconditionally so the manual `/api/pollers/pr-mention/tick`
  // endpoint can drive jobs end-to-end in dev. Whether the poller starts on its own
  // is gated by `prMentionEnabled` + `!isDev` below.
  const prMentionPipeline = new PrMentionPipeline({
    agentCli,
    repoRegistry,
    getGitHubToken,
    workdirPath: config.workdirPath,
    slackNotifier,
    slackPrCommentChannelId: config.slackPrCommentChannelId,
    getPendingCount: () => queue.pendingCount(),
    logger: logger.child({ module: "pr-mention-pipeline" }),
    metrics,
  });

  const datadogLogsClient =
    config.datadogApiKey.length > 0 && config.datadogAppKey.length > 0
      ? new DatadogLogsClient(config.datadogApiKey, config.datadogAppKey, config.datadogSite)
      : null;

  // SlackMentionPipeline only needs `slackNotifier` to function. We build it
  // whenever Slack is wired up so manual ticks work in dev even with mentions
  // polling off.
  const slackMentionPipeline = slackNotifier
    ? new SlackMentionPipeline({
        metrics,
        agentCli,
        repoRegistry,
        getGitHubToken,
        workdirPath: config.workdirPath,
        slackNotifier,
        jiraClient,
        datadogLogsClient,
        logger: logger.child({ module: "slack-mention-pipeline" }),
      })
    : undefined;

  const authProbes: AuthProbe[] = [
    {
      integration: "jira",
      check: async () => (await jiraClient.resolveAccountIdForBotEmail(config.jiraEmail)) != null,
    },
    {
      integration: "github",
      check: async () => {
        try {
          const token = await getGitHubToken();
          const res = await fetch("https://api.github.com/rate_limit", {
            headers: {
              Authorization: `Bearer ${token}`,
              Accept: "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
            },
          });
          return res.ok;
        } catch {
          return false;
        }
      },
    },
    {
      // Probe the ACTIVE provider's API: a cursor-auth failure is irrelevant
      // (and an anthropic one invisible) when the bot is running on Claude.
      integration: "agent",
      check:
        config.agentProvider === "claude"
          ? async () => {
              try {
                const baseUrl = config.anthropicBaseUrl ?? "https://api.anthropic.com";
                const res = await fetch(`${baseUrl}/v1/models`, {
                  headers: {
                    "x-api-key": config.anthropicApiKey,
                    "anthropic-version": "2023-06-01",
                  },
                });
                return res.ok;
              } catch {
                return false;
              }
            }
          : () => cursorCloudClient.checkAuth(),
    },
  ];
  if (slackNotifier) {
    authProbes.push({ integration: "slack", check: () => slackNotifier.checkAuth() });
  }

  const authChecker = new AuthChecker({
    metrics,
    logger: logger.child({ module: "auth-checker" }),
    intervalMs: OBSERVABILITY.AUTH_CHECK_INTERVAL_MS,
    probes: authProbes,
  });
  authChecker.start();

  let ready = false;
  const deferredPollerStarts: Array<() => void> = [];

  queue.start(async (job, signal) => {
    let result;
    if (job.jobKind === "jira") {
      result = await pipeline.execute(job, signal);
    } else if (job.jobKind === "jira_ticket_review") {
      result = await reviewTicketPipeline.execute(job, signal);
    } else if (job.jobKind === "pr_comment") {
      result = await prCommentPipeline.execute(job, signal);
    } else if (job.jobKind === "pr_mention") {
      result = await prMentionPipeline.execute(job, signal);
    } else if (job.jobKind === "slack_mention" && slackMentionPipeline) {
      result = await slackMentionPipeline.execute(job, signal);
    } else {
      result = {
        success: false,
        issueKey: "unknown",
        outcome: "completed" as const,
        abortReason: `No handler for ${job.jobKind}`,
      };
    }
    if (!result.success) {
      logger.warn(
        { issueKey: result.issueKey, reason: result.abortReason },
        "Pipeline finished without success",
      );
    }
    return result.outcome;
  });

  const poller = new JiraPoller({
    jiraClient,
    queue,
    metrics,
    botEmail: config.jiraEmail,
    squadScope: config.squadScope,
    getGitHubToken,
    repoRegistry,
    buildGitHubClient: (token, repoUrl, ghLogger) =>
      config.sandboxMode
        ? new SandboxGitHubClient(token, repoUrl, config.jiraDomain, ghLogger)
        : new GitHubClientImpl(token, repoUrl, config.jiraDomain, ghLogger),
    logger: logger.child({ module: "poller" }),
  });

  if (config.jiraPollEnabled && !config.isDev) {
    deferredPollerStarts.push(() => poller.start());
  } else if (config.isDev && config.jiraPollEnabled) {
    logger.info("Jira poller force-disabled (dev mode)");
  } else {
    logger.info("Jira poller disabled via JIRA_POLL_ENABLED=false");
  }

  const prCommentPoller = new PrCommentPoller({
    repoRegistry,
    getGitHubToken,
    queue,
    metrics,
    githubBotUser: config.githubBotUser,
    ignoredCommentLogins: config.ignoredCommentLogins,
    squadScope: config.squadScope,
    logger: logger.child({ module: "pr-comment-poller" }),
  });
  if (config.prReviewEnabled && !config.isDev) {
    deferredPollerStarts.push(() => prCommentPoller.start());
  } else if (config.isDev && config.prReviewEnabled) {
    logger.info("PR review poller force-disabled (dev mode)");
  } else {
    logger.info("PR review poller disabled via ATTEND_FEEDBACK_IN_PR_ENABLED=false");
  }

  const ticketReviewPoller = new JiraTicketReviewPoller({
    jiraClient,
    queue,
    metrics,
    botEmail: config.jiraEmail,
    squadScope: config.squadScope,
    logger: logger.child({ module: "ticket-review-poller" }),
  });
  if (config.jiraTicketReviewEnabled && !config.isDev) {
    deferredPollerStarts.push(() => ticketReviewPoller.start());
  } else if (config.isDev && config.jiraTicketReviewEnabled) {
    logger.info("Jira ticket review poller force-disabled (dev mode)");
  } else {
    logger.info("Jira ticket review poller disabled via JIRA_TICKET_REVIEW_ENABLED=false");
  }

  // Construct the Slack mention poller as long as the minimum deps exist
  // (Slack notifier + bot user id) so the manual `/api/pollers/slack-mention/tick`
  // endpoint can run it in dev. The interval is started only when the toggle is
  // on AND we're not in dev mode.
  const slackMentionPoller =
    slackNotifier && config.slackBotUserId.length > 0
      ? new SlackMentionPoller({
          slackNotifier,
          queue,
          metrics,
          slackBotUserId: config.slackBotUserId,
          logger: logger.child({ module: "slack-mention-poller" }),
        })
      : undefined;

  if (slackMentionPoller && config.slackMentionsEnabled && !config.isDev) {
    deferredPollerStarts.push(() => slackMentionPoller.start());
  } else if (slackMentionPoller && config.isDev && config.slackMentionsEnabled) {
    logger.info("Slack mention poller force-disabled (dev mode)");
  } else if (config.slackMentionsEnabled && !config.slackBotUserId) {
    logger.warn("Slack mentions enabled but SLACK_BOT_USER_ID not set");
  }

  const prMentionPoller = new PrMentionPoller({
    repoRegistry,
    getGitHubToken,
    queue,
    metrics,
    githubBotUser: config.githubBotUser,
    ignoredCommentLogins: config.ignoredCommentLogins,
    logger: logger.child({ module: "pr-mention-poller" }),
  });

  if (config.prMentionEnabled && !config.isDev) {
    deferredPollerStarts.push(() => prMentionPoller.start());
  } else if (config.isDev && config.prMentionEnabled) {
    logger.info("PR mention poller force-disabled (dev mode)");
  } else {
    logger.info("PR mention poller disabled via PR_MENTION_ENABLED=false");
  }

  const app = createApp({
    isReady: () => ready,
    queue,
    jiraClient,
    cursorCloudClient,
    pipeline,
    repoRegistry,
    repoSelector,
    getGitHubToken,
    jiraDomain: config.jiraDomain,
    workdirPath: config.workdirPath,
    githubBotUser: config.githubBotUser,
    logger: logger.child({ module: "server" }),
    datadogLogsClient,
    slackNotifier,
    slackBotToken: config.slackBotToken,
    slackDailyReportChannelId: config.slackDailyReportChannelId ?? config.slackChannelId,
    squadScope: config.squadScope,
    botEmail: config.jiraEmail,
    jiraPoller: poller,
    ticketReviewPoller,
    prCommentPoller,
    prMentionPoller,
    slackMentionPoller,
    dailyReportScheduler,
  });

  const server = serve({
    fetch: app.fetch,
    port: config.port,
    // Datadog log searches (window=24h) can paginate through many API calls;
    // 120 s gives headroom well beyond the 60 s per-page abort timeout.
    idleTimeout: 120,
  });

  logger.info(
    { port: server.port, version: BOT_VERSION, instanceId: INSTANCE_ID, env: BOT_ENV },
    `server_started v${BOT_VERSION} (${INSTANCE_ID}) [${BOT_ENV}] on :${server.port}`,
  );

  const reposToWarm = repoRegistry.getAll();
  logger.info({ total: reposToWarm.length }, "prewarm_starting");
  const prewarmStart = Date.now();
  const warmed = await prewarmRepos(
    reposToWarm,
    config.workdirPath,
    logger.child({ module: "prewarm" }),
    getGitHubToken,
  );
  const prewarmDurationMs = Date.now() - prewarmStart;
  metrics.gauge(METRIC.prewarmWarmed, warmed, { total: reposToWarm.length });
  metrics.timing(METRIC.prewarmDuration, prewarmDurationMs, { total: reposToWarm.length });
  logger.info(
    { warmed, total: reposToWarm.length, durationMs: prewarmDurationMs },
    "prewarm_finished_marking_ready",
  );

  ready = true;

  for (const start of deferredPollerStarts) start();

  logger.info(
    {
      warmed,
      total: reposToWarm.length,
      durationMs: prewarmDurationMs,
      pollersStarted: deferredPollerStarts.length,
    },
    "ready_accepting_work",
  );

  let shuttingDown = false;
  const shutdown = async (signal: string) => {
    if (shuttingDown) return;
    shuttingDown = true;
    logger.info(
      { signal, version: BOT_VERSION, instanceId: INSTANCE_ID, env: BOT_ENV },
      `shutting_down v${BOT_VERSION} (${INSTANCE_ID}) [${BOT_ENV}]`,
    );
    poller.stop();
    ticketReviewPoller.stop();
    prCommentPoller.stop();
    slackMentionPoller?.stop();
    prMentionPoller.stop();
    authChecker.stop();
    dailyReportScheduler?.stop();
    promptCleanup.stop();
    stopCodeArtifactRefresh?.();
    await queue.shutdown(SHUTDOWN.DRAIN_GRACE_MS);
    server.stop();
    process.exit(0);
  };

  process.on("SIGINT", () => void shutdown("SIGINT"));
  process.on("SIGTERM", () => void shutdown("SIGTERM"));
}

main();
