// 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"
] as const);

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

export type RecorderManifestPayload =
  | string
  | {
      target?: string;
      replacement?: string;
      start?: string;
      end?: string;
      find?: string;
      content?: 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"
      ? { ok: true, action }
      : { ok: false, error: `${action}_payload_must_be_string`, message: `${action} requires string Payload.` };
  }

  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 visit = (entry: unknown) => {
    if (isManifestLike(entry)) found.push(entry as RecorderManifest);

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

    if (!entry || typeof entry !== "object") {
      if (typeof entry === "string") {
        const clean = entry.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "");
        if (!clean) return;
        try { visit(JSON.parse(clean)); } catch {}
      }
      return;
    }

    for (const child of Object.values(entry as Record<string, any>)) visit(child);
  };

  visit(value);
  return found;
}
