import type { CoreDispatchMessage, JsonValue } from "../../shared/src/index";
import { isProviderDispatchCommand } from "../../shared/src/index";
import { ctx, sql, logger, config } from "./context";
import type { RawTaskRow, ProviderSocket } from "./types";
import { jsonResponse, errorToJson } from "./http";
import { recordEvent } from "./db";

function isTerminalTaskStatus(status: string): boolean {
  return status === "succeeded" || status === "failed";
}

export async function rawTask(taskId: string): Promise<RawTaskRow | null> {
  const rows = await sql()<RawTaskRow[]>`
    SELECT id, provider_id, command, status, payload, result, updated_at
    FROM unidesk_tasks
    WHERE id = ${taskId}
    LIMIT 1
  `;
  return rows[0] ?? null;
}

export async function notifyTaskTerminal(taskId: string): Promise<void> {
  const waiters = ctx.taskTerminalWaiters.get(taskId);
  if (waiters === undefined || waiters.size === 0) return;
  const task = await rawTask(taskId);
  if (task === null || !isTerminalTaskStatus(task.status)) return;
  ctx.taskTerminalWaiters.delete(taskId);
  for (const waiter of waiters) waiter(task);
}

export async function waitForTaskTerminal(taskId: string, timeoutMs: number): Promise<RawTaskRow | null> {
  let latest = await rawTask(taskId);
  if (latest !== null && isTerminalTaskStatus(latest.status)) return latest;
  return await new Promise<RawTaskRow | null>((resolve) => {
    let settled = false;
    let timer: ReturnType<typeof setTimeout>;
    const settle = (task: RawTaskRow | null): void => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      const waiters = ctx.taskTerminalWaiters.get(taskId);
      waiters?.delete(settle);
      if (waiters !== undefined && waiters.size === 0) ctx.taskTerminalWaiters.delete(taskId);
      resolve(task);
    };
    const waiters = ctx.taskTerminalWaiters.get(taskId) ?? new Set<(task: RawTaskRow | null) => void>();
    waiters.add(settle);
    ctx.taskTerminalWaiters.set(taskId, waiters);
    timer = setTimeout(() => {
      rawTask(taskId)
        .then((task) => settle(task ?? latest))
        .catch(() => settle(latest));
    }, Math.max(1, timeoutMs));
  });
}

export async function createAndSendTask(
  providerId: string,
  command: CoreDispatchMessage["command"],
  payload: Record<string, JsonValue>,
): Promise<{ taskId: string; providerOnline: boolean }> {
  const taskId = `task_${Date.now()}_${Math.random().toString(16).slice(2)}`;
  await sql()`
    INSERT INTO unidesk_tasks (id, provider_id, command, status, payload, result)
    VALUES (${taskId}, ${providerId}, ${command}, 'queued', ${sql().json(payload)}, NULL)
  `;
  const socket = ctx.activeProviders.get(providerId);
  if (!socket) {
    await recordEvent("task_queued_provider_offline", providerId, { taskId, providerId, command });
    return { taskId, providerOnline: false };
  }
  const dispatch: CoreDispatchMessage = { type: "dispatch", taskId, command, payload };
  socket.send(JSON.stringify(dispatch));
  await sql()`
    UPDATE unidesk_tasks
    SET status = 'dispatched', updated_at = now()
    WHERE id = ${taskId} AND status = 'queued'
  `;
  await recordEvent("task_dispatched", providerId, { taskId, providerId, command });
  return { taskId, providerOnline: true };
}

export async function markStaleTasksFailed(): Promise<void> {
  if (!ctx.dbReady) return;
  const timeoutMs = config().taskPendingTimeoutMs;
  const rows = await sql()<Array<{ id: string; provider_id: string; command: string; previous_status: string; updated_at: Date | string }>>`
    WITH stale AS (
      SELECT id, provider_id, command, status AS previous_status, updated_at
      FROM unidesk_tasks
      WHERE status IN ('queued', 'dispatched', 'running')
        AND updated_at < now() - (${timeoutMs}::bigint * interval '1 millisecond')
      FOR UPDATE
    ),
    updated AS (
      UPDATE unidesk_tasks task
      SET
        status = 'failed',
        result = jsonb_build_object(
          'error', 'task timed out without terminal provider status',
          'timeoutMs', ${timeoutMs}::bigint,
          'previousStatus', stale.previous_status,
          'previousResult', task.result,
          'timedOutAt', now()
        ),
        updated_at = now()
      FROM stale
      WHERE task.id = stale.id
      RETURNING task.id, task.provider_id, task.command, stale.previous_status, stale.updated_at
    )
    SELECT * FROM updated
  `;
  for (const row of rows) {
    await recordEvent("task_timeout", row.provider_id, {
      taskId: row.id,
      providerId: row.provider_id,
      command: row.command,
      previousStatus: row.previous_status,
      previousUpdatedAt: row.updated_at instanceof Date ? row.updated_at.toISOString() : String(row.updated_at),
      timeoutMs,
    });
    notifyTaskTerminal(row.id).catch((error) => logger("error", "task_waiter_notify_failed", { taskId: row.id, error: errorToJson(error) }));
  }
}

export async function providerSupports(providerId: string, capability: string): Promise<boolean> {
  if (!ctx.dbReady) return false;
  const rows = await sql()<Array<{ labels: unknown }>>`
    SELECT labels
    FROM unidesk_nodes
    WHERE provider_id = ${providerId}
    LIMIT 1
  `;
  const labels = rows[0]?.labels;
  if (typeof labels !== "object" || labels === null || Array.isArray(labels)) return false;
  const capabilities = (labels as Record<string, unknown>).unideskCapabilities;
  return Array.isArray(capabilities) ? capabilities.filter((item): item is string => typeof item === "string").includes(capability) : false;
}

export async function dispatchTask(req: Request): Promise<Response> {
  const body = (await req.json()) as { providerId?: unknown; command?: unknown; payload?: unknown };
  const providerId = typeof body.providerId === "string" ? body.providerId : "";
  const command = isProviderDispatchCommand(body.command) ? body.command : null;
  const payload = typeof body.payload === "object" && body.payload !== null ? (body.payload as Record<string, JsonValue>) : {};
  if (!providerId) {
    return jsonResponse({ ok: false, error: "providerId is required" }, 400);
  }
  if (command === null) {
    return jsonResponse({ ok: false, error: "command must be one of docker.ps, provider.upgrade, host.ssh, microservice.http, echo" }, 400);
  }
  if (command === "host.ssh" && !(await providerSupports(providerId, "host.ssh"))) {
    return jsonResponse({ ok: false, error: `provider does not declare host.ssh capability: ${providerId}` }, 409);
  }
  if (command === "microservice.http" && !(await providerSupports(providerId, "microservice.http"))) {
    return jsonResponse({ ok: false, error: `provider does not declare microservice.http capability: ${providerId}` }, 409);
  }
  const { taskId, providerOnline } = await createAndSendTask(providerId, command, payload);
  return jsonResponse({ ok: true, taskId, status: providerOnline ? "dispatched" : "queued", providerOnline });
}
