/**
 * @fileoverview Record-Replay V3 composition root (bootstrap)
 * @description
 * Wires storage, events, scheduler, triggers and RPC for the MV3 background service worker.
 *
 * Design description:
 * - You must first execute recoverFromCrash() and then start scheduler.start()
 * - Use global singleton keepalive-manager to avoid multiple controller conflicts
 * - RunExecutor Use RunRunner to execute the actual Flow
 */

import type { UnixMillis } from "./domain/json";
import type { RunId } from "./domain/ids";
import { RR_ERROR_CODES, createRRError, type RRError } from "./domain/errors";

import type { StoragePort } from "./engine/storage/storage-port";
import {
  StorageBackedEventsBus,
  type EventsBus,
} from "./engine/transport/events-bus";

import { DEFAULT_QUEUE_CONFIG, type RunQueueItem } from "./engine/queue/queue";
import {
  createLeaseManager,
  generateOwnerId,
  type LeaseManager,
} from "./engine/queue/leasing";
import {
  createRunScheduler,
  type RunExecutor,
  type RunScheduler,
} from "./engine/queue/scheduler";
import { recoverFromCrash } from "./engine/recovery/recovery-coordinator";

import { RpcServer } from "./engine/transport/rpc-server";

import {
  createTriggerManager,
  type TriggerManager,
} from "./engine/triggers/trigger-manager";
import { createUrlTriggerHandlerFactory } from "./engine/triggers/url-trigger";
import { createCommandTriggerHandlerFactory } from "./engine/triggers/command-trigger";
import { createContextMenuTriggerHandlerFactory } from "./engine/triggers/context-menu-trigger";
import { createDomTriggerHandlerFactory } from "./engine/triggers/dom-trigger";
import { createIntervalTriggerHandlerFactory } from "./engine/triggers/interval-trigger";
import { createOnceTriggerHandlerFactory } from "./engine/triggers/once-trigger";
import { createManualTriggerHandlerFactory } from "./engine/triggers/manual-trigger";

import { createChromeArtifactService } from "./engine/kernel/artifacts";
import {
  createRunRunnerFactory,
  type RunRunnerFactory,
} from "./engine/kernel/runner";
import {
  createDebugController,
  createRunnerRegistry,
  type DebugController,
  type RunnerRegistry,
} from "./engine/kernel/debug-controller";

import { PluginRegistry } from "./engine/plugins/registry";
import {
  registerReplayNodes,
  DEFAULT_REPLAY_NODE_EXCLUDE_LIST,
} from "./engine/plugins/register-replay-nodes";

import { acquireKeepalive } from "../keepalive-manager";
import { createStoragePort } from "./index";
import { calculateWorkflowRevision } from "./flows/publish";
import { WorkflowSecretRefError, resolveWorkflowSecretRefs } from "./secrets";

// ==================== Types ====================

type Logger = Pick<Console, "debug" | "info" | "warn" | "error">;

/**
 * V3 runtime handle
 */
export interface V3Runtime {
  ownerId: string;
  storage: StoragePort;
  events: EventsBus;
  leaseManager: LeaseManager;
  scheduler: RunScheduler;
  runners: RunnerRegistry;
  debugController: DebugController;
  triggers: TriggerManager;
  rpcServer: RpcServer;
  stop(): Promise<void>;
}

// ==================== Singleton State ====================

let runtime: V3Runtime | null = null;
let bootstrapPromise: Promise<V3Runtime> | null = null;

// ==================== Utilities ====================

function errorMessage(err: unknown): string {
  if (err instanceof Error) return err.message;
  if (err && typeof err === "object" && "message" in err)
    return String((err as { message: unknown }).message);
  return String(err);
}

function createSecretRefRunError(error: WorkflowSecretRefError): RRError {
  const code =
    error.code === RR_ERROR_CODES.SECRET_REF_NOT_FOUND ||
    error.code === RR_ERROR_CODES.SECRET_REF_EXPIRED ||
    error.code === RR_ERROR_CODES.SECRET_REF_REVOKED ||
    error.code === RR_ERROR_CODES.SECRET_REF_SCOPE_MISMATCH
      ? error.code
      : RR_ERROR_CODES.SECRET_REF_INVALID;
  return createRRError(code, error.message, {
    retryable: false,
    data: {
      path: error.path,
      ...(error.secretRef ? { secretRef: error.secretRef } : {}),
    },
  });
}

function isFiniteNumber(v: unknown): v is number {
  return typeof v === "number" && Number.isFinite(v);
}

async function tabExists(tabId: number): Promise<boolean> {
  try {
    await chrome.tabs.get(tabId);
    return true;
  } catch {
    return false;
  }
}

async function createEphemeralTab(logger: Logger): Promise<number> {
  const tab = await chrome.tabs.create({ url: "about:blank", active: false });
  if (tab.id === undefined) {
    throw new Error("chrome.tabs.create returned a tab without id");
  }
  logger.debug(`[RR-V3] Allocated ephemeral tab ${tab.id}`);
  return tab.id;
}

async function safeRemoveTab(tabId: number, logger: Logger): Promise<void> {
  try {
    await chrome.tabs.remove(tabId);
  } catch (e) {
    logger.debug(`[RR-V3] Failed to close tab ${tabId}:`, e);
  }
}

/**
 * Resolve the Tab ID required to run Run
 * Priority: run.tabId > queue.tabId > trigger.sourceTabId > Create new tab
 */
async function resolveRunTab(input: {
  runTabId?: number;
  queueTabId?: number;
  triggerTabId?: number;
  logger: Logger;
}): Promise<{ tabId: number; shouldClose: boolean }> {
  const candidates = [
    input.runTabId,
    input.queueTabId,
    input.triggerTabId,
  ].filter((x): x is number => isFiniteNumber(x));

  for (const tabId of candidates) {
    if (await tabExists(tabId)) {
      return { tabId, shouldClose: false };
    }
  }

  const tabId = await createEphemeralTab(input.logger);
  return { tabId, shouldClose: true };
}

/**
 * Mark Run as failed
 * NOTE: The latest RunRecord will be re-read to get the correct startedAt
 */
async function failRun(
  deps: {
    storage: StoragePort;
    events: EventsBus;
    now: () => UnixMillis;
    logger: Logger;
  },
  runId: RunId,
  error: RRError,
): Promise<void> {
  const finishedAt = deps.now();

  // Re-fetch the latest run record to get the correct startedAt
  let startedAt = finishedAt;
  try {
    const latestRun = await deps.storage.runs.get(runId);
    if (latestRun?.startedAt !== undefined) {
      startedAt = latestRun.startedAt;
    }
  } catch {
    // ignore - use finishedAt as startedAt
  }

  const tookMs = Math.max(0, finishedAt - startedAt);

  try {
    await deps.storage.runs.patch(runId, {
      status: "failed",
      finishedAt,
      tookMs,
      error,
    });
  } catch (e) {
    deps.logger.error(`[RR-V3] Failed to patch run "${runId}" as failed:`, e);
    return;
  }

  try {
    await deps.events.append({ runId, type: "run.failed", error });
  } catch (e) {
    deps.logger.warn(`[RR-V3] Failed to append run.failed for "${runId}":`, e);
  }
}

// ==================== Run Executor ====================

/**
 * Create a default RunExecutor
 * Use RunRunner to execute Flow
 */
function createDefaultRunExecutor(deps: {
  storage: StoragePort;
  events: EventsBus;
  runnerFactory: RunRunnerFactory;
  runners: RunnerRegistry;
  now: () => UnixMillis;
  logger: Logger;
}): RunExecutor {
  return async (item: RunQueueItem): Promise<void> => {
    const runId = item.id;

    // 1. Get RunRecord
    const run = await deps.storage.runs.get(runId);
    if (!run) {
      deps.logger.warn(
        `[RR-V3] RunRecord not found for queue item "${runId}", skipping execution`,
      );
      return;
    }

    // 2. GetFlow
    const flow = await deps.storage.flows.get(item.flowId);
    if (!flow) {
      await failRun(
        deps,
        runId,
        createRRError(
          RR_ERROR_CODES.VALIDATION_ERROR,
          `Flow "${item.flowId}" not found`,
        ),
      );
      return;
    }
    const expectedRevision = run.expectedRevision ?? item.expectedRevision;
    if (expectedRevision) {
      const currentRevision = calculateWorkflowRevision(flow);
      if (currentRevision !== expectedRevision) {
        await failRun(
          deps,
          runId,
          createRRError(
            RR_ERROR_CODES.STALE_WORKFLOW_DESCRIPTOR,
            `Flow "${item.flowId}" changed before execution; expected revision ${expectedRevision}, current ${currentRevision}`,
            {
              retryable: true,
              data: {
                flowId: item.flowId,
                expectedRevision,
                currentRevision,
              },
            },
          ),
        );
        return;
      }
    }

    // 3. Parse Tab ID
    const { tabId, shouldClose } = await resolveRunTab({
      runTabId: run.tabId,
      queueTabId: item.tabId,
      triggerTabId: item.trigger?.sourceTabId,
      logger: deps.logger,
    });

    // 4. Synchronize attempt to RunRecord
    try {
      await deps.storage.runs.patch(runId, {
        attempt: item.attempt,
        maxAttempts: item.maxAttempts,
        tabId,
      });
    } catch (e) {
      deps.logger.debug(
        `[RR-V3] Failed to patch run "${runId}" attempt/tabId:`,
        e,
      );
    }

    // 5. Execute Run
    let runner;
    try {
      const runtimeArgs = await resolveWorkflowSecretRefs(item.args);
      runner = deps.runnerFactory.create(runId, {
        flow,
        tabId,
        args: runtimeArgs,
        recordArgs: item.args,
        execution: run.execution ?? item.execution,
        startNodeId: run.startNodeId,
        stopBeforeNodeId: run.stopBeforeNodeId,
        endNodeId: run.endNodeId,
        debug: item.debug,
      });

      // Registered to RunnerRegistry for use by DebugController and RPC
      deps.runners.register(runId, runner);

      await runner.start();
    } catch (e) {
      await failRun(
        deps,
        runId,
        e instanceof WorkflowSecretRefError
          ? createSecretRefRunError(e)
          : createRRError(
              RR_ERROR_CODES.INTERNAL,
              `Executor crashed: ${errorMessage(e)}`,
            ),
      );
    } finally {
      // 6. Log out Runner
      if (runner) {
        deps.runners.unregister(runId);
      }

      // 7. Clean up temporary tabs
      if (shouldClose) {
        await safeRemoveTab(tabId, deps.logger);
      }
    }
  };
}

// ==================== Bootstrap ====================

/**
 * Starting the RR-V3 runtime
 * @returns runtime handle
 */
export async function bootstrapV3(): Promise<V3Runtime> {
  if (runtime) return runtime;
  if (bootstrapPromise) return bootstrapPromise;

  bootstrapPromise = (async () => {
    const logger: Logger = console;
    const now = (): UnixMillis => Date.now();

    logger.info("[RR-V3] Bootstrapping...");

    // 1) Storage
    const storage = createStoragePort();

    // 2) EventsBus
    const events: EventsBus = new StorageBackedEventsBus(storage.events);

    try {
      const expired = await storage.artifacts.cleanupExpired(now());
      const overLimit = await storage.artifacts.enforceRetention();
      const deleted = expired + overLimit;
      if (deleted > 0) {
        logger.debug(`[RR-V3] Cleaned ${deleted} expired/over-limit artifact(s)`);
      }
    } catch (e) {
      logger.warn(
        `[RR-V3] Artifact cleanup failed: ${e instanceof Error ? e.message : String(e)}`,
      );
    }

    // 3) Lease owner identity (per SW instance)
    const ownerId = generateOwnerId();
    logger.debug(`[RR-V3] Owner ID: ${ownerId}`);

    // 4) LeaseManager
    const leaseManager = createLeaseManager(
      storage.queue,
      DEFAULT_QUEUE_CONFIG,
    );

    // 5) RunnerRegistry + DebugController
    const runners = createRunnerRegistry();
    const debugController = createDebugController({ storage, events, runners });

    // 6) Keepalive (reuse global singleton to avoid multiple controllers fighting)
    const keepalive = {
      acquire: (tag: string) => acquireKeepalive(`rr_v3:${tag}`),
    };

    // 7) PluginRegistry - register replay action handlers as runtime nodes
    const plugins = new PluginRegistry();
    const registeredNodes = registerReplayNodes(plugins, {
      // Exclude control directives that V3 runner doesn't support
      exclude: [...DEFAULT_REPLAY_NODE_EXCLUDE_LIST],
    });
    logger.debug(
      `[RR-V3] Registered ${registeredNodes.length} replay action handlers as runtime nodes`,
    );

    // 8) RunExecutor via RunRunnerFactory
    const runnerFactory = createRunRunnerFactory({
      storage,
      events,
      plugins,
      artifactService: createChromeArtifactService({ store: storage.artifacts }),
      now,
    });

    const execute = createDefaultRunExecutor({
      storage,
      events,
      runnerFactory,
      runners,
      now,
      logger,
    });

    // 7) Scheduler
    const scheduler = createRunScheduler({
      queue: storage.queue,
      leaseManager,
      keepalive,
      config: DEFAULT_QUEUE_CONFIG,
      ownerId,
      execute,
      now,
      logger,
    });

    // 8) TriggerManager
    const triggers = createTriggerManager({
      storage,
      events,
      scheduler,
      handlerFactories: {
        url: createUrlTriggerHandlerFactory({ logger }),
        command: createCommandTriggerHandlerFactory({ logger }),
        contextMenu: createContextMenuTriggerHandlerFactory({ logger }),
        dom: createDomTriggerHandlerFactory({ logger }),
        interval: createIntervalTriggerHandlerFactory({ logger }),
        once: createOnceTriggerHandlerFactory({ logger }),
        manual: createManualTriggerHandlerFactory({ logger }),
      },
      now,
      logger,
    });

    // 10) RpcServer (created but started after recovery)
    const rpcServer = new RpcServer({
      storage,
      events,
      scheduler,
      debugController,
      runners,
      triggerManager: triggers,
      now,
    });

    // Cleanup helper for error recovery
    const cleanup = async (): Promise<void> => {
      try {
        rpcServer.stop();
      } catch {
        /* ignore */
      }
      try {
        await triggers.stop();
      } catch {
        /* ignore */
      }
      try {
        scheduler.stop();
      } catch {
        /* ignore */
      }
      try {
        leaseManager.dispose();
      } catch {
        /* ignore */
      }
      try {
        debugController.stop();
      } catch {
        /* ignore */
      }
    };

    try {
      // 10) Recovery - MUST run before scheduler.start()
      logger.info("[RR-V3] Running crash recovery...");
      await recoverFromCrash({ storage, events, ownerId, now, logger });

      // 11) Start components
      scheduler.start();
      await triggers.start();
      rpcServer.start();

      logger.info("[RR-V3] Bootstrap complete");
    } catch (e) {
      await cleanup();
      throw e;
    }

    // Build runtime handle
    runtime = {
      ownerId,
      storage,
      events,
      leaseManager,
      scheduler,
      runners,
      debugController,
      triggers,
      rpcServer,
      stop: async () => {
        logger.info("[RR-V3] Stopping...");
        // Stop order: RPC first (block new requests) -> triggers -> scheduler -> lease -> debug
        rpcServer.stop();
        await triggers.stop().catch(() => {});
        scheduler.stop();
        leaseManager.dispose();
        debugController.stop();
        runtime = null;
        logger.info("[RR-V3] Stopped");
      },
    };

    return runtime;
  })().finally(() => {
    bootstrapPromise = null;
  });

  return bootstrapPromise;
}

/**
 * Get the current runtime (if started)
 */
export function getV3Runtime(): V3Runtime | null {
  return runtime;
}

/**
 * Check if V3 is started
 */
export function isV3Running(): boolean {
  return runtime !== null;
}
