// deckControl.ts
// Deck request control layer.
// Deck owns route registration. deckControl owns intake, local checks, server run authorization, and reply relay.
// Recorder does not import this file.

import fs from "node:fs";
import { wakeRecorder, getRecorderStatus } from "./recorder";
import { capture } from "./capture";
import { controller } from "./controller";
import { scanManifestCandidates, validateManifestShape } from "./manifestContract";
import { SettingsPaths } from "./pathList";
import { getSettings } from "./mgrSNP";
import { getAuthority } from "./hypauth/auth";
import { authorizeRun } from "./hypauth/hypauthcall";
import type { TapeKind } from "./playback";

export type RecordSource = "manual" | "tape" | "amp" | "aux" | "update";

export type DeckControlConfig = {
  ampAuth: boolean;
};

const config: DeckControlConfig = { ampAuth: true };

const AMP_TOKEN_PROTECTED_PREFIXES = [
  "/record",
  "/manual/record",
  "/bridge/record",
  "/aux/linein",
  "/aux/lineout",
  "/import/record",
  "/remote/record",
  "/update/record",
  "/read/",
  "/logs/",
  "/verbatim/output"
];

function isObject(value: unknown): value is Record<string, any> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

function readJson(filePath: string): any {
  try { return JSON.parse(fs.readFileSync(filePath, "utf8")); }
  catch { return {}; }
}

function normSource(source: RecordSource): RecordSource {
  return ["manual", "tape", "amp", "aux", "update"].includes(source) ? source : "manual";
}

function sourceLabel(source: RecordSource): string {
  if (source === "amp") return "bridge";
  if (source === "aux") return "extension";
  if (source === "tape") return "monitor";
  if (source === "update") return "update";
  return "deck";
}

function needsServer(source: RecordSource): boolean {
  return source === "amp" || source === "aux" || source === "update";
}

function isLocal(req: any): boolean {
  const ip = String(req?.ip || "").replace(/^::ffff:/, "");
  const host = String(req?.headers?.host || "").toLowerCase();
  return ip === "127.0.0.1" || ip === "::1" || ip === "localhost" || host.startsWith("127.0.0.1:") || host.startsWith("localhost:");
}

function isAmpPath(pathname: string): boolean {
  const clean = String(pathname || "").split("?")[0];
  return AMP_TOKEN_PROTECTED_PREFIXES.some(prefix => clean === prefix.replace(/\/$/, "") || clean.startsWith(prefix));
}

function ampToken(req: any): { expected: string; received: string } {
  const user = readJson(SettingsPaths.user);
  const raw = req?.headers?.["x-amp-token"];
  return {
    expected: String(process.env.AMP_TOKEN || user?.bridgeAuth?.apiKey || "").trim(),
    received: Array.isArray(raw) ? String(raw[0] || "") : String(raw || "")
  };
}

function shellMode(): "deny" | "standard" | "extended" {
  const mode = String(getSettings()?.recorder?.shellAccess || "deny").trim().toLowerCase();
  return mode === "standard" || mode === "extended" ? mode : "deny";
}

function isShell(candidate: unknown): boolean {
  return isObject(candidate) && String(candidate.Action || "").trim().toLowerCase() === "shell";
}

function firstAction(candidates: unknown[]): string {
  for (const candidate of candidates) {
    if (isObject(candidate) && candidate.Action) return String(candidate.Action || "").trim();
  }
  return "record";
}

export function configureDeckControl(next: Partial<DeckControlConfig>): DeckControlConfig {
  if (typeof next.ampAuth === "boolean") config.ampAuth = next.ampAuth;
  return getDeckControlConfig();
}

export function getDeckControlConfig(): DeckControlConfig {
  return { ...config };
}

export function bodyOf(req: any): Record<string, any> {
  return isObject(req?.body) ? req.body : {};
}

export function ampOut(reply: any, code: number, error: string, message?: string, extra: Record<string, any> = {}) {
  return reply.code(code).send({ ok: false, error, ...(message ? { message } : {}), ...extra });
}

export function relayResult(result: any, reply: any, extra: Record<string, any> = {}) {
  if (!result?.ok) return reply.code(400).send({ ...(result || {}), ...extra });
  return { ...result, ...extra };
}

export const relayRecorderResult = relayResult;

export async function ampIn(req: any, reply: any) {
  if (!config.ampAuth) return;
  if (String(req?.method || "").toUpperCase() === "OPTIONS") return;

  const pathname = String(req?.url || "").split("?")[0];
  if (!isAmpPath(pathname) || isLocal(req)) return;

  const token = ampToken(req);
  if (!token.expected || token.received !== token.expected) {
    capture.system({ tag: "DeckControl", event: "amp_auth_rejected", path: pathname, ip: req?.ip });
    return ampOut(reply, 401, "unauthorized");
  }
}

function caseValue(item: unknown, names: string[] = []): unknown {
  if (!isObject(item)) return undefined;
  const wanted = new Set(names.map(name => String(name || "").toLowerCase()));
  for (const [key, value] of Object.entries(item)) {
    if (wanted.has(String(key || "").toLowerCase())) return value;
  }
  return undefined;
}

function nonTaskDubKind(item: unknown): TapeKind | "" {
  if (!isObject(item)) return "";
  if (String(caseValue(item, ["Task", "task"]) || "").trim()) return "";
  if (String(caseValue(item, ["Diagnostic", "diagnostic"]) || "").trim()) return "diagnostic";
  if (String(caseValue(item, ["Enhancement", "enhancement"]) || "").trim()) return "enhancement";
  return "";
}

function nonTaskDubCandidates(body: unknown): Array<{ manifest: any; kind: TapeKind }> {
  const manifests = scanManifestCandidates(body);
  const candidates = manifests.length ? manifests : [body];
  const items = candidates.map(manifest => ({ manifest, kind: nonTaskDubKind(manifest) }));
  return items.length && items.every(item => item.kind === "diagnostic" || item.kind === "enhancement")
    ? items as Array<{ manifest: any; kind: TapeKind }>
    : [];
}

function validateContract(body: unknown, source: RecordSource) {
  const manifests = scanManifestCandidates(body);
  const candidates = manifests.length ? manifests : [body];

  for (const candidate of candidates) {
    const contract = validateManifestShape(candidate, { requireTask: true, requirePayload: true });
    if (!contract.ok) {
      const previewValue = typeof body === "string"
        ? body
        : isObject(body)
          ? JSON.stringify(body).slice(0, 900)
          : String(body ?? "");

      capture.system({
        tag: "DeckControl",
        event: "contract_rejected",
        source,
        error: contract.error,
        bodyType: Array.isArray(body) ? "array" : typeof body,
        bodyKeys: isObject(body) ? Object.keys(body).join(",") : "",
        candidateType: Array.isArray(candidate) ? "array" : typeof candidate,
        candidateKeys: isObject(candidate) ? Object.keys(candidate).join(",") : "",
        preview: previewValue.slice(0, 240).replace(/\s+/g, " ")
      });

      capture.manifest("DeckControl", "contract_rejected_body", {
        source,
        error: contract.error,
        body,
        candidate
      });

      return {
        ok: false,
        candidates,
        error: contract.error || "invalid_manifest",
        message: contract.message || "Invalid recorder manifest."
      };
    }
  }

  return { ok: true, candidates };
}

function validateShell(candidates: unknown[], source: RecordSource) {
  if (shellMode() !== "deny" || !candidates.some(isShell)) return { ok: true };

  capture.system({ tag: "DeckControl", event: "shell_rejected", source, reason: "shell_access_denied_by_settings" });
  return {
    ok: false,
    error: "shell_access_denied_by_settings",
    message: "Shell commands are disabled in Recorder settings."
  };
}

export async function recordIn(body: unknown, source: RecordSource, reply: any) {
  const cleanSource = normSource(source);

  const dubs = nonTaskDubCandidates(body);
  if (dubs.length) {
    const items = [];
    for (const item of dubs) {
      items.push(await capture.tape.queueProposal({
        manifest: item.manifest,
        kind: item.kind,
        source: sourceLabel(cleanSource),
        recorderStatus: "NO"
      }));
    }
    capture.system({ tag: "DeckControl", event: "non_task_manifest_routed_to_dubs", source: cleanSource, count: items.length, kinds: dubs.map(item => item.kind).join(",") });
    return { ok: true, routed: "dubs", accepted: items.length, items, deckControl: { source: cleanSource } };
  }

  const contract = validateContract(body, cleanSource);
  if (!contract.ok) return ampOut(reply, 400, contract.error, contract.message);

  const shell = validateShell(contract.candidates, cleanSource);
  if (!shell.ok) return ampOut(reply, 403, shell.error, shell.message);

  const rails = controller.validateRails();
  if (!rails.ok) {
    capture.system({ tag: "DeckControl", event: "rails_rejected", source: cleanSource, error: rails.error });
    return ampOut(reply, rails.error === "project_required_for_writes" ? 400 : 403, rails.error || "rails_blocked", rails.message, {
      state: rails.state,
      rails: rails.rails
    });
  }

  let runAuthority: any = null;
  if (needsServer(cleanSource)) {
    runAuthority = await authorizeRun({
      source: cleanSource,
      action: firstAction(contract.candidates)
    });

    if (!runAuthority?.allowed) {
      capture.system({ tag: "DeckControl", event: "run_rejected", source: cleanSource, reason: runAuthority?.reason });
      return ampOut(reply, Number(runAuthority?.status || 403), runAuthority?.reason || "access_not_authorized", runAuthority?.message || "Run not authorized.", {
        authority: runAuthority?.authority || getAuthority()
      });
    }
  }

  const recorderInput = contract.candidates.length === 1 ? contract.candidates[0] : contract.candidates;
  const result = await wakeRecorder(recorderInput, sourceLabel(cleanSource));
  if (!result.ok) return reply.code(400).send(result);

  return relayResult(result, reply, {
    deckControl: { source: cleanSource },
    rails: rails.rails,
    authority: runAuthority?.authority || getAuthority(),
    ...(runAuthority ? { runAuthority: { allowed: true, reason: runAuthority.reason, message: runAuthority.message } } : {})
  });
}

export function recordRoute(source: RecordSource) {
  return async (req: any, reply: any) => recordIn(req.body, source, reply);
}

export async function recordCheck() {
  const rails = controller.validateRails();

  if (!rails.ok && rails.error === "project_required_for_writes") {
    return {
      ok: true,
      projectRequired: true,
      message: rails.message || "Select a project to continue.",
      state: rails.state,
      deckControl: getDeckControlConfig(),
      authority: getAuthority(),
      ...getRecorderStatus()
    };
  }

  return {
    ...(await wakeRecorder()),
    deckControl: getDeckControlConfig(),
    rails,
    authority: getAuthority()
  };
}
