import { createHash, randomUUID } from "crypto";

import { getDbInstance, rowToCamel } from "./core";
import { decrypt, encrypt } from "./encryption";

export type CommandCodeAuthStatus = "pending" | "received" | "applied" | "expired";

export interface CommandCodeAuthMetadata {
  userId?: string;
  userName?: string;
  keyName?: string;
  receivedAt?: string;
}

export interface CommandCodeAuthSafeStatus {
  id: string;
  stateHash: string;
  status: CommandCodeAuthStatus;
  metadata: CommandCodeAuthMetadata | null;
  createdAt: string;
  expiresAt: string;
  receivedAt: string | null;
  appliedAt: string | null;
  updatedAt: string;
}

export interface ConsumedCommandCodeAuthSecret extends CommandCodeAuthSafeStatus {
  apiKey: string;
}

type DbRunResult = { changes?: number };
type DbStatement<TRow = unknown> = {
  get: (...params: unknown[]) => TRow | undefined;
  all: (...params: unknown[]) => TRow[];
  run: (...params: unknown[]) => DbRunResult;
};
type DbLike = {
  prepare: <TRow = unknown>(sql: string) => DbStatement<TRow>;
  transaction: <T extends (...args: unknown[]) => unknown>(fn: T) => T;
};

type AuthSessionRow = {
  id: string;
  state_hash: string;
  status: CommandCodeAuthStatus;
  encrypted_api_key?: string | null;
  metadata_json?: string | null;
  created_at: string;
  expires_at: string;
  received_at?: string | null;
  applied_at?: string | null;
  updated_at: string;
};

function db(): DbLike {
  return getDbInstance() as unknown as DbLike;
}

export function hashCommandCodeAuthState(state: string): string {
  return createHash("sha256").update(state, "utf8").digest("hex");
}

function nowIso(): string {
  return new Date().toISOString();
}

function parseMetadata(value: unknown): CommandCodeAuthMetadata | null {
  if (!value) return null;
  // rowToCamel auto-parses the `metadata_json` column and exposes the object under
  // `camel.metadata` (already parsed); accept that directly. Fall back to parsing a
  // raw string for any other caller.
  if (typeof value === "object" && !Array.isArray(value)) {
    return value as CommandCodeAuthMetadata;
  }
  if (typeof value !== "string") return null;
  try {
    const parsed = JSON.parse(value) as CommandCodeAuthMetadata;
    return parsed && typeof parsed === "object" ? parsed : null;
  } catch {
    return null;
  }
}

function toSafeStatus(row: AuthSessionRow): CommandCodeAuthSafeStatus {
  const camel = rowToCamel(row) as Record<string, unknown>;
  return {
    id: String(camel.id),
    stateHash: String(camel.stateHash),
    status: camel.status as CommandCodeAuthStatus,
    metadata: parseMetadata(camel.metadata ?? camel.metadataJson),
    createdAt: String(camel.createdAt),
    expiresAt: String(camel.expiresAt),
    receivedAt: (camel.receivedAt as string | null | undefined) ?? null,
    appliedAt: (camel.appliedAt as string | null | undefined) ?? null,
    updatedAt: String(camel.updatedAt),
  };
}

function markExpiredForState(stateHash: string, now = nowIso()): void {
  db()
    .prepare(
      `UPDATE command_code_auth_sessions
       SET status = 'expired', updated_at = ?
       WHERE state_hash = ? AND status IN ('pending', 'received') AND expires_at <= ?`
    )
    .run(now, stateHash, now);
}

export function createPendingCommandCodeAuthSession(input: {
  stateHash: string;
  expiresAt: string;
}): CommandCodeAuthSafeStatus {
  const id = randomUUID();
  const now = nowIso();
  db()
    .prepare(
      `INSERT INTO command_code_auth_sessions (
        id, state_hash, status, encrypted_api_key, metadata_json,
        created_at, expires_at, received_at, applied_at, updated_at
      ) VALUES (?, ?, 'pending', NULL, NULL, ?, ?, NULL, NULL, ?)`
    )
    .run(id, input.stateHash, now, input.expiresAt, now);

  const row = db()
    .prepare<AuthSessionRow>("SELECT * FROM command_code_auth_sessions WHERE id = ?")
    .get(id);
  if (!row) throw new Error("Failed to create Command Code auth session");
  return toSafeStatus(row);
}

export function markCommandCodeAuthSessionReceived(input: {
  stateHash: string;
  apiKey: string;
  metadata?: CommandCodeAuthMetadata;
}): CommandCodeAuthSafeStatus | null {
  const now = nowIso();
  markExpiredForState(input.stateHash, now);
  const metadata: CommandCodeAuthMetadata = {
    ...(input.metadata || {}),
    receivedAt: now,
  };
  const encryptedApiKey = encrypt(input.apiKey);
  db()
    .prepare(
      `UPDATE command_code_auth_sessions
       SET status = 'received', encrypted_api_key = ?, metadata_json = ?, received_at = ?, updated_at = ?
       WHERE state_hash = ? AND status IN ('pending', 'received') AND expires_at > ?`
    )
    .run(encryptedApiKey, JSON.stringify(metadata), now, now, input.stateHash, now);

  return getCommandCodeAuthSessionSafeStatus(input.stateHash);
}

export function getCommandCodeAuthSessionSafeStatus(
  stateHash: string
): CommandCodeAuthSafeStatus | null {
  markExpiredForState(stateHash);
  const row = db()
    .prepare<AuthSessionRow>("SELECT * FROM command_code_auth_sessions WHERE state_hash = ?")
    .get(stateHash);
  return row ? toSafeStatus(row) : null;
}

export function consumeCommandCodeAuthSecret(
  stateHash: string
): ConsumedCommandCodeAuthSecret | null {
  const database = db();
  return database.transaction(() => {
    const now = nowIso();
    database
      .prepare(
        `UPDATE command_code_auth_sessions
         SET status = 'expired', updated_at = ?
         WHERE state_hash = ? AND status IN ('pending', 'received') AND expires_at <= ?`
      )
      .run(now, stateHash, now);

    const row = database
      .prepare<AuthSessionRow>(
        `SELECT * FROM command_code_auth_sessions
         WHERE state_hash = ? AND status = 'received' AND expires_at > ? AND encrypted_api_key IS NOT NULL`
      )
      .get(stateHash, now);
    if (!row?.encrypted_api_key) return null;

    const apiKey = decrypt(row.encrypted_api_key);
    if (!apiKey) return null;

    const result = database
      .prepare(
        `UPDATE command_code_auth_sessions
         SET status = 'applied', encrypted_api_key = NULL, applied_at = ?, updated_at = ?
         WHERE id = ? AND status = 'received'`
      )
      .run(now, now, row.id);
    if (!result.changes) return null;

    return {
      ...toSafeStatus({
        ...row,
        status: "applied",
        encrypted_api_key: null,
        applied_at: now,
        updated_at: now,
      }),
      apiKey,
    };
  })() as ConsumedCommandCodeAuthSecret | null;
}

export function cleanupExpiredCommandCodeAuthSessions(now = nowIso()): number {
  const result = db()
    .prepare(
      `UPDATE command_code_auth_sessions
       SET status = 'expired', updated_at = ?
       WHERE status IN ('pending', 'received') AND expires_at <= ?`
    )
    .run(now, now);
  return result.changes ?? 0;
}
