import { loadConfig, resolveBindHost } from "./config";
import { loadOrCreateToken } from "./token";
import { buildScheduler, buildState } from "./collector";
import { AdapterRegistry } from "./registry";
import { buildAdapters, resolveOffloadConnection } from "./adapters";
import type { HarnessAdapter } from "./adapters/harness";
import { createKanboardClient } from "./incidents/kanboard-client";
import { loadIncidentsConnection } from "./incidents/connection";
import { createIncidentsProvider, type IncidentsProvider } from "./incidents/provider";
import { loadIncidentOptions } from "./incidents/dispatch-options";
import { createIncidentDispatchLauncher } from "./incidents/incident-launcher";
import { createIncidentStatusWriter } from "./incidents/status-writer";
import { IncidentStatusStore } from "./incidents/status-store";
import { IncidentMutationStore } from "./incidents/incident-mutation-store";
import { RequestsStore } from "./requests/requests-store";
import { ReportHistoryStore } from "./reports/history-store";
import { checkIncidentsReadiness } from "./incidents/bootstrap";
import { deployDir, incidentMutationDbFile, incidentStatusDbFile, reportHistoryDbFile, requestsDbFile, stateDir } from "./paths";
import { join } from "node:path";
import { mkdirSync } from "node:fs";

import { createActionGateway } from "./actions";
import {
  ALERTER_MINING_OUTPUT_ABSENT,
  createAlerter,
  resolveAlerterSettings,
} from "./alerter";
import { PermissionQueue } from "./permissions";
import { Worker } from "node:worker_threads";
import { configuredFactoryDbPath } from "./adapters/factory";
import { startServer, type FactoryDetailWorkerResult } from "./server";
import { CollectorFatalError } from "./errors";

const DIGEST_TIME_ZONE = process.env.OVERDECK_DIGEST_TZ ?? "Asia/Bangkok";

function readFactoryRunDetail(
  adwId: string,
  dbPath: string,
  signal: AbortSignal,
): Promise<FactoryDetailWorkerResult> {
  return new Promise((resolve, reject) => {
    const worker = new Worker(new URL("./factory-detail-worker.ts", import.meta.url), {
      workerData: { adwId, dbPath },
    });
    let settled = false;
    const abort = () => {
      if (settled) return;
      settled = true;
      void worker.terminate();
      reject(new DOMException("Factory detail request aborted", "AbortError"));
    };
    signal.addEventListener("abort", abort, { once: true });
    worker.once("message", (result: FactoryDetailWorkerResult) => {
      if (settled) return;
      settled = true;
      signal.removeEventListener("abort", abort);
      resolve(result);
    });
    worker.once("error", (error) => {
      if (settled) return;
      settled = true;
      signal.removeEventListener("abort", abort);
      reject(error);
    });
    worker.once("exit", (code) => {
      if (!settled && code !== 0) {
        settled = true;
        signal.removeEventListener("abort", abort);
        reject(new Error(`factory detail worker exited ${code}`));
      }
    });
  });
}

function main(): void {
  const config = loadConfig();
  const token = loadOrCreateToken();
  const state = buildState();
  const offloadConnection = resolveOffloadConnection(config, token);
  const requests = new RequestsStore(requestsDbFile());
  let reportHistory: ReportHistoryStore | undefined;
  try {
    reportHistory = new ReportHistoryStore(reportHistoryDbFile());
  } catch (error) {
    console.error("report history store initialization failed", error);
  }

  const registry = new AdapterRegistry();
  for (const adapter of buildAdapters(config, token, requests)) {
    registry.register(adapter);
  }
  const scheduler = buildScheduler(registry.list(), state);
  scheduler.start();

  const host = resolveBindHost(config);
  const harness = registry.get("harness") as HarnessAdapter | undefined;
  // Hooks reach the collector over loopback regardless of the advertised bind host.
  const permissions = new PermissionQueue({
    state,
    collectorUrl: `http://127.0.0.1:${config.port}`,
    token,
  });
  const incidentsConnection = loadIncidentsConnection();
  const incidentWorkspaceRoot = join(stateDir(), "incidents", "worktrees");
  mkdirSync(incidentWorkspaceRoot, { recursive: true, mode: 0o700 });
  const incidentsClient = incidentsConnection ? createKanboardClient({ baseUrl: incidentsConnection.baseUrl, token: incidentsConnection.token, fetchImpl: fetch }) : undefined;
  const incidentStatusStore = incidentsClient ? new IncidentStatusStore(incidentStatusDbFile()) : undefined;
  const incidentMutationStore = incidentsClient ? new IncidentMutationStore(incidentMutationDbFile()) : undefined;
  const incidentStatusWriter = incidentsClient && incidentStatusStore
    ? (async () => {
        let retryDelayMs = 1_000;
        for (;;) {
          try {
            const readiness = await checkIncidentsReadiness(incidentsClient);
            return createIncidentStatusWriter(incidentsClient, readiness, incidentStatusStore);
          } catch (error) {
            console.error("incident status writer initialization failed; retrying", error);
            await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
            retryDelayMs = Math.min(retryDelayMs * 2, 30_000);
          }
        }
      })()
    : undefined;
  void incidentStatusWriter?.then((writer) => {
    let reconciling = false;
    const reconcile = async () => {
      if (reconciling) return;
      reconciling = true;
      try {
        await writer.reconcile();
      } catch (error) {
        console.error("incident status reconciliation failed", error);
      } finally {
        reconciling = false;
      }
    };
    void reconcile();
    setInterval(() => { void reconcile(); }, 30_000);
  }).catch((error) => console.error("incident status writer initialization failed", error));
  const incidents: IncidentsProvider | undefined = incidentsClient
    ? createIncidentsProvider({
        client: incidentsClient,
        loadIncidentOptions,
        mutationStore: incidentMutationStore,
        launcher: createIncidentDispatchLauncher({ workspaceRoot: incidentWorkspaceRoot, sourceRepo: deployDir(), runnerEntry: join(import.meta.dir, "incidents", "incident-runner-main.ts") }),
        workspaceFor: (incidentId) => join(incidentWorkspaceRoot, incidentId),
        wrapperFor: (relativeWrapper) => join(deployDir(), "modules", "harness", relativeWrapper),
        recordStatus: async (update) => (await incidentStatusWriter!)(update),

      })
    : undefined;

  const reconcileIncidentResolutions = incidents?.reconcileResolutions?.bind(incidents);
  if (reconcileIncidentResolutions) {
    let reconcilingResolutions = false;
    const reconcileResolutions = async () => {
      if (reconcilingResolutions) return;
      reconcilingResolutions = true;
      try {
        await reconcileIncidentResolutions();
      } catch (error) {
        console.error("incident resolution reconciliation failed", error);
      } finally {
        reconcilingResolutions = false;
      }
    };
    void reconcileResolutions();
    setInterval(() => { void reconcileResolutions(); }, 30_000);
  }

  const actionGateway = createActionGateway({
    state,
    harness,
    permissions,
    controllerUrl: offloadConnection.controllerUrl,
    controllerToken: offloadConnection.token,
    fetcher: fetch,
    incidents,
  });

  const alerterSettings = resolveAlerterSettings();
  if (alerterSettings.ok) {
    createAlerter({
      state,
      config: alerterSettings.config,
      timeZone: DIGEST_TIME_ZONE,
    });
  } else if (alerterSettings.code === ALERTER_MINING_OUTPUT_ABSENT) {
    console.error(`[${alerterSettings.code}] ${alerterSettings.message}`);
  }

  startServer({
    host,
    port: config.bind_port,
    token,
    state,
    actionGateway,
    permissions,
    harness,
    digestTimeZone: DIGEST_TIME_ZONE,
    incidents,
    requests,
    reportHistory,
    loadIncidentOptions: incidents ? loadIncidentOptions : undefined,
    controllerUrl: offloadConnection.controllerUrl,
    controllerToken: offloadConnection.token,
    fetcher: fetch,
    loadFactoryRunDetail: (adwId, signal) => readFactoryRunDetail(
      adwId,
      configuredFactoryDbPath(config),
      signal,
    ),
  });

  console.log(`overdeck-collector listening on ${host}:${config.bind_port}`);
}

try {
  main();
} catch (err) {
  if (err instanceof CollectorFatalError) {
    console.error(`[${err.code}] ${err.message}`);
    process.exit(1);
  }
  throw err;
}
