// manifestContract.ts
// Scope: recorder manifest contract only.
// Owns shape/action/payload validation. Does not authorize sender, scope, tier, or writes.

export const RECORDER_ACTIONS = new Set([
  "newfile",
  "rewritefile",
  "replaceblock",
  "replacestring",
  "replaceall",
  "replacefunction",
  "insertbefore",
  "insertafter",
  "shell"
] as const);

export type RecorderAction =
  | "newfile"
  | "rewritefile"
  | "replaceblock"
  | "replacestring"
  | "replaceall"
  | "replacefunction"
  | "insertbefore"
  | "insertafter"
  | "shell";

export type RecorderManifestPayload =
  | string
  | {
      target?: string;
      input?: string;
      replacement?: string;
      start?: string;
      retain?: string;
      retained?: string;
      find?: string;
      content?: string;
      command?: string;
      args?: string[];
    };

export type RecorderManifest = {
  ID?: string;
  Task?: string | null;
  Action: RecorderAction;
  FilePath: string;
  Payload: RecorderManifestPayload;
  Comment?: string | null;
  [key: string]: any;
};

export type ManifestValidation = {
  ok: boolean;
  error?: string;
  message?: string;
  action?: RecorderAction;
};

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

export function normalizeAction(value: unknown): RecorderAction | "" {
  const action = String(value || "").trim().toLowerCase();
  return RECORDER_ACTIONS.has(action as RecorderAction) ? action as RecorderAction : "";
}

export function isManifestLike(value: unknown): value is RecorderManifest {
  if (!isObject(value)) return false;
  return Boolean(String(value.Action || "").trim() && String(value.FilePath || "").trim());
}

export function validateManifestShape(value: unknown, opts: { requireTask?: boolean; requirePayload?: boolean } = {}): ManifestValidation {
  if (!isObject(value)) return { ok: false, error: "missing manifest", message: "Manifest must be an object." };

  const action = normalizeAction(value.Action);
  if (!action) return { ok: false, error: "unsupported_recorder_action", message: "Manifest Action is missing or unsupported." };

  if (!String(value.FilePath || "").trim()) {
    return { ok: false, error: "recorder_filepath_missing", message: "Manifest FilePath is required." };
  }

  if (opts.requireTask && !String(value.Task || value.task || "").trim()) {
    return { ok: false, error: "missing Task", message: "Manifest Task is required." };
  }

  if (opts.requirePayload && !("Payload" in value)) {
    return { ok: false, error: "payload_missing", message: "Manifest Payload is required." };
  }

  return { ok: true, action };
}

export function validatePayloadShape(manifest: RecorderManifest): ManifestValidation {
  const base = validateManifestShape(manifest, { requirePayload: true });
  if (!base.ok) return base;

  const action = base.action!;
  const payload = manifest.Payload;

  if (action === "newfile" || action === "rewritefile") {
    return typeof payload === "string" || (isObject(payload) && typeof payload.input === "string")
      ? { ok: true, action }
      : { ok: false, error: `${action}_payload_must_be_input`, message: `${action} requires string Payload or Payload.input.` };
  }

  if (!isObject(payload) && typeof payload !== "string") {
    return { ok: false, error: `${action}_payload_must_be_object`, message: `${action} requires object Payload.` };
  }

  return { ok: true, action };
}

export function scanManifestCandidates(value: unknown): RecorderManifest[] {
  const found: RecorderManifest[] = [];
  const seen = new WeakSet<object>();

  const parseJsonText = (entry: string): unknown[] => {
    const raw = String(entry || "").trim();
    if (!raw) return [];

    const candidates: string[] = [
      raw,
      raw.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim()
    ];

    const fencePattern = /```(?:json)?\s*([\s\S]*?)\s*```/gi;
    let fence: RegExpExecArray | null = null;
    while ((fence = fencePattern.exec(raw))) {
      if (fence[1]) candidates.push(fence[1].trim());
    }

    const firstObject = raw.indexOf("{");
    const lastObject = raw.lastIndexOf("}");
    if (firstObject !== -1 && lastObject > firstObject) candidates.push(raw.slice(firstObject, lastObject + 1));

    const firstArray = raw.indexOf("[");
    const lastArray = raw.lastIndexOf("]");
    if (firstArray !== -1 && lastArray > firstArray) candidates.push(raw.slice(firstArray, lastArray + 1));

    const parsed: unknown[] = [];
    const unique = [...new Set(candidates.filter(Boolean))];

    for (const candidate of unique) {
      try { parsed.push(JSON.parse(candidate)); }
      catch {}
    }

    return parsed;
  };

  const visit = (entry: unknown) => {
    if (typeof entry === "string") {
      parseJsonText(entry).forEach(visit);
      return;
    }

    if (isManifestLike(entry)) {
      found.push(entry as RecorderManifest);
      return;
    }

    if (Array.isArray(entry)) {
      entry.forEach(visit);
      return;
    }

    if (!entry || typeof entry !== "object") return;
    if (seen.has(entry)) return;
    seen.add(entry);

    const body = entry as Record<string, any>;

    [
      body.manifest,
      body.Manifest,
      body.recordingSheet,
      body.RecordingSheet,
      body.sheet,
      body.Sheet,
      body.packet,
      body.Packet,
      body.item,
      body.row,
      body.payload,
      body.Payload,
      body.block,
      body.body,
      body.text,
      body.content,
      body.message,
      body.output
    ].forEach(visit);

    for (const child of Object.values(body)) visit(child);
  };

  visit(value);
  return found;
}