import { cronMatches, nextCronOccurrence } from './cron.js';
import { getJobHandler, getRegisteredJobs } from './registry.js';
import * as repo from './repository.js';
import * as runTracker from '../runs/tracker.js';
import type { JobDefinitionDoc, JobRunDoc, JobContext, JobResult } from './types.js';

// ── In-Process Job Runner ────────────────────────────────────
// Tick loop evaluates cron expressions every minute and runs due jobs.
// Uses a simple lock flag to prevent overlapping ticks.

const DEFAULT_PRODUCT_ID = 'lysnrai';
const MAX_CONCURRENT_JOBS = 5;
const STUCK_JOB_THRESHOLD_MS = 600_000; // 10 minutes

let tickInterval: ReturnType<typeof globalThis.setInterval> | null = null;
let isRunning = false;
let isShuttingDown = false;

// Track active job executions for concurrency + heartbeat
const activeJobs = new Map<string, { jobName: string; startedAt: number; runId: string }>();

/**
 * Get the number of currently running jobs.
 */
export function getActiveJobCount(): number {
  return activeJobs.size;
}

/**
 * Get info about all active jobs (for diagnostics).
 */
export function getActiveJobs(): Array<{ jobName: string; runId: string; elapsedMs: number }> {
  const now = Date.now();
  return Array.from(activeJobs.values()).map(j => ({
    jobName: j.jobName,
    runId: j.runId,
    elapsedMs: now - j.startedAt,
  }));
}

/**
 * Start the job runner tick loop. Evaluates every 60 seconds.
 */
export function startRunner(log: {
  info: (...a: unknown[]) => void;
  warn: (...a: unknown[]) => void;
  error: (...a: unknown[]) => void;
}): void {
  if (tickInterval) return;
  isShuttingDown = false;

  log.info('[jobs] Starting job runner (60s tick, max concurrency: %d)', MAX_CONCURRENT_JOBS);

  // Run immediately on start, then every 60s
  tick(log).catch(() => {});
  tickInterval = globalThis.setInterval(() => {
    tick(log).catch(() => {});
  }, 60_000);
}

/**
 * Stop the job runner gracefully. Waits for active jobs to complete (up to 30s).
 */
export async function stopRunner(): Promise<void> {
  isShuttingDown = true;
  if (tickInterval) {
    globalThis.clearInterval(tickInterval);
    tickInterval = null;
  }

  // Wait for active jobs to finish (up to 30 seconds)
  const deadline = Date.now() + 30_000;
  while (activeJobs.size > 0 && Date.now() < deadline) {
    await new Promise(resolve => globalThis.setTimeout(resolve, 500));
  }
}

/**
 * Single tick: check all enabled jobs, run those whose cron matches now.
 */
async function tick(log: {
  info: (...a: unknown[]) => void;
  warn: (...a: unknown[]) => void;
  error: (...a: unknown[]) => void;
}): Promise<void> {
  if (isRunning) return;
  isRunning = true;

  try {
    if (isShuttingDown) return;

    const now = new Date();
    // Round to current minute for matching
    now.setUTCSeconds(0, 0);

    const registered = getRegisteredJobs();
    if (registered.length === 0) return;

    // Recover stuck jobs (running longer than threshold)
    for (const [key, job] of activeJobs) {
      if (Date.now() - job.startedAt > STUCK_JOB_THRESHOLD_MS) {
        log.warn(
          { jobName: job.jobName, runId: job.runId, elapsedMs: Date.now() - job.startedAt },
          '[jobs] Removing stuck job from active tracking'
        );
        activeJobs.delete(key);
      }
    }

    // Get definitions from DB (or create defaults)
    let definitions: JobDefinitionDoc[];
    try {
      definitions = await repo.listJobDefinitions(DEFAULT_PRODUCT_ID);
    } catch {
      // Cosmos not available (e.g., in tests) — skip
      return;
    }

    for (const def of definitions) {
      if (def.status !== 'enabled') continue;
      if (!getJobHandler(def.name)) continue;

      // Enforce concurrency limit
      if (activeJobs.size >= MAX_CONCURRENT_JOBS) {
        log.warn(
          '[jobs] Concurrency limit reached (%d), skipping remaining jobs',
          MAX_CONCURRENT_JOBS
        );
        break;
      }

      // Skip if this job is already running
      if (activeJobs.has(def.name)) continue;

      if (cronMatches(def.cronExpression, now)) {
        executeJob(def, 'scheduler', log).catch(() => {});
      }
    }
  } finally {
    isRunning = false;
  }
}

/**
 * Execute a job by name. Called by the tick loop or manually via admin API.
 */
export async function executeJob(
  def: JobDefinitionDoc,
  triggeredBy: 'scheduler' | 'manual',
  log: {
    info: (...a: unknown[]) => void;
    warn: (...a: unknown[]) => void;
    error: (...a: unknown[]) => void;
  }
): Promise<JobRunDoc> {
  const handler = getJobHandler(def.name);
  if (!handler) {
    throw new Error(`No handler registered for job '${def.name}'`);
  }

  const runId = crypto.randomUUID();
  const startedAt = new Date().toISOString();

  // Track this job as active
  activeJobs.set(def.name, { jobName: def.name, startedAt: Date.now(), runId });

  // Create run record
  const run: JobRunDoc = {
    id: runId,
    pk: `${def.productId}:${def.name}`,
    jobName: def.name,
    productId: def.productId,
    status: 'running',
    startedAt,
    triggeredBy,
  };

  try {
    await repo.createJobRun(run);
  } catch {
    // Non-fatal — continue execution
  }

  try {
    await runTracker.startRun({
      id: runId,
      productId: def.productId,
      kind: 'job',
      name: def.name,
      source: 'platform.jobs',
      triggeredBy,
      input: {
        jobDefinitionId: def.id,
        cronExpression: def.cronExpression,
      },
      metadata: {
        timeoutMs: def.timeoutMs,
        retryOnFailure: def.retryOnFailure,
        maxRetries: def.maxRetries,
      },
    });
    await runTracker.startRunStep({
      runId,
      productId: def.productId,
      stepName: 'execute',
      order: 1,
      input: {
        triggeredBy,
      },
    });
  } catch {
    // Non-fatal
  }

  // Mark definition as running
  try {
    await repo.updateJobDefinition(def.id, def.productId, {
      status: 'running',
      lastRunAt: startedAt,
      lastRunStatus: 'running',
    });
  } catch {
    // Non-fatal
  }

  // Execute with timeout
  const context: JobContext = {
    jobName: def.name,
    runId,
    productId: def.productId,
    log,
  };

  let result: JobResult;
  const start = Date.now();

  try {
    const controller = new globalThis.AbortController();
    const timeout = globalThis.setTimeout(() => controller.abort(), def.timeoutMs || 300_000);

    result = await Promise.race([
      handler(context),
      new Promise<JobResult>((_, reject) => {
        controller.signal.addEventListener('abort', () => {
          reject(new Error(`Job '${def.name}' timed out after ${def.timeoutMs || 300_000}ms`));
        });
      }),
    ]);

    globalThis.clearTimeout(timeout);
  } catch (err: unknown) {
    result = {
      success: false,
      message: err instanceof Error ? err.message : String(err),
    };
  }

  // Remove from active tracking
  activeJobs.delete(def.name);

  const durationMs = Date.now() - start;
  const completedAt = new Date().toISOString();
  const status = result.success ? 'success' : 'failed';

  // Update run record
  const updatedRun: JobRunDoc = {
    ...run,
    status,
    completedAt,
    durationMs,
    error: result.success ? undefined : result.message,
    metrics: result.metrics,
  };

  try {
    await repo.updateJobRun(updatedRun);
  } catch {
    // Non-fatal
  }

  try {
    if (result.success) {
      await runTracker.completeRunStep(runId, def.productId, 'execute', {
        message: result.message ?? 'completed',
        ...(result.metrics ? { metrics: result.metrics } : {}),
      });
      await runTracker.completeRun(runId, def.productId, {
        message: result.message ?? 'completed',
        ...(result.metrics ? { metrics: result.metrics } : {}),
      });
    } else {
      await runTracker.failRunStep(
        runId,
        def.productId,
        'execute',
        result.message ?? 'Job failed',
        result.metrics ? { metrics: result.metrics } : undefined
      );
      await runTracker.failRun(
        runId,
        def.productId,
        result.message ?? 'Job failed',
        result.metrics ? { metrics: result.metrics } : undefined
      );
    }
  } catch {
    // Non-fatal
  }

  // Update definition
  const nextRun = nextCronOccurrence(def.cronExpression, new Date());
  try {
    await repo.updateJobDefinition(def.id, def.productId, {
      status: 'enabled',
      lastRunAt: startedAt,
      lastRunStatus: status,
      nextRunAt: nextRun?.toISOString(),
    });
  } catch {
    // Non-fatal
  }

  log.info({ jobName: def.name, runId, status, durationMs }, `[jobs] ${def.name} ${status}`);

  return updatedRun;
}

/**
 * Ensure all registered jobs have definitions in Cosmos.
 * Creates missing definitions with default cron.
 */
export async function ensureJobDefinitions(
  defaults: Array<{ name: string; cron: string; description?: string; timeoutMs?: number }>
): Promise<void> {
  for (const def of defaults) {
    const id = `job_${def.name}`;
    const now = new Date().toISOString();
    const next = nextCronOccurrence(def.cron, new Date());

    try {
      await repo.upsertJobDefinition({
        id,
        productId: DEFAULT_PRODUCT_ID,
        name: def.name,
        description: def.description,
        cronExpression: def.cron,
        status: 'enabled',
        createdAt: now,
        updatedAt: now,
        timeoutMs: def.timeoutMs ?? 300_000,
        retryOnFailure: false,
        maxRetries: 0,
        nextRunAt: next?.toISOString(),
      });
    } catch {
      // May already exist — that's fine
    }
  }
}
