// manifold.ts
// Scope: Relay Manifold — the single communication and distribution terminal
// between the customer machine and Nodarama server.
//
// Responsibilities:
//   1. Receive internal routing envelopes from EQ
//   2. Scrub all real content from context — only structural shape crosses the wire
//   3. Send scrubbed shape + hyp token to Nodarama server
//   4. Receive RoutePackage (prompts, thresholds, strategy) from server
//   5. Dispatch RoutePackage back to the correct EQ handler via `via` field
//
// Nothing real ever leaves the machine.
// The `via` field is internal manifold language only — never in the external contract.

import { logSystem } from "./capture";
import { getRemoteAccessToken, getUserTier } from "./hypauth/auth";
import { loadSettings } from "./power";

const NV_SERVER       = "https://api.nodarama.com";
const ROUTE_TIMEOUT   = 8000;
const CLIENT_VER_PATH = "./package.json";

// ── Types ─────────────────────────────────────────────────────────────────────

// Internal routing instruction — manifold's own language, never external
export type ManifoldVia =
  | "AI1A"
  | "AI1B"
  | "AI1C"
  | "AI1OTHER"
  | "AI2chat"
  | "AI3diagnostic";

// What EQ sends into the manifold
export type ManifoldEnvelope = {
  via:          ManifoldVia;
  contextShape: Record<string, any>;   // scrubbed — no real content
  hypToken:     string;                // structural identity token
  ts:           number;
};

// What the server returns — the full instruction package for the local AI call
export type RoutePackage = {
  via:          ManifoldVia;
  systemPrompt: string;
  strategy:     string;
  threshold:    number;
  mode:         string;
  lane:         string;
  offline:      boolean;
  meta?:        Record<string, any>;
};

// What EQ passes in to request a route
export type ManifoldRequest = {
  via:          ManifoldVia;
  pathway:      string;
  context:      Record<string, any>;   // real context — scrubbed internally
  hypToken:     string;
};

// ── Hyp token builder ─────────────────────────────────────────────────────────

const TRIGGER_CODE: Record<string, string> = {
  AI1A:        "E",
  AI1B:        "E",
  AI1C:        "E",
  AI1OTHER:    "E",
  AI2chat:     "C",
  AI3diagnostic: "B"
};

const ACTION_CLASS: Record<string, string> = {
  insertbefore:    "I",
  insertafter:     "I",
  insertbeforeit:  "I",
  insertafterit:   "I",
  replacestring:   "II",
  replaceblock:    "II",
  replacefunction: "IV",
  newfile:         "III",
  rewritefile:     "III",
  composite:       "V",
  unknown:         "II"
};

function randomAlpha(len: number): string {
  const chars = "ABCDEFGHJKLMNPQRSTVWXYZ23456789";
  return Array.from({ length: len }, () =>
    chars[Math.floor(Math.random() * chars.length)]
  ).join("");
}

export function buildHypToken(via: ManifoldVia, actionType: string, itemCount: number): string {
  const t  = TRIGGER_CODE[via] ?? "E";
  const a  = ACTION_CLASS[actionType.toLowerCase()] ?? ACTION_CLASS.unknown;
  const d  = Math.min(9, Math.max(1, itemCount));
  const id = randomAlpha(4);
  return `${t}${a}${d}${id}`;
}

// ── Scrubber ──────────────────────────────────────────────────────────────────
// Strips all real content from a context object.
// Preserves only structural shape: key names, value types, array lengths,
// action/error classification codes, file extensions (not names), item counts.
// No string values that could contain real content survive this pass.

const SAFE_STRING_KEYS = new Set([
  "lane", "mode", "pathway", "via", "stage",
  "actionType", "errorClass", "fileExtension",
  "requestMode"
]);

const SAFE_CODE_PATTERN = /^[A-Z0-9_]{1,32}$/;

function scrubValue(key: string, value: unknown, depth: number): unknown {
  if (depth > 6) return typeof value;

  if (value === null)      return null;
  if (value === undefined) return undefined;

  if (typeof value === "boolean") return value;
  if (typeof value === "number")  return typeof value;  // preserve type, not value

  if (typeof value === "string") {
    if (SAFE_STRING_KEYS.has(key)) return value;
    if (SAFE_CODE_PATTERN.test(value) && value.length <= 32) return value;
    // Preserve file extension only, not name
    if (key === "filePath" || key === "FilePath") {
      const ext = value.match(/\.[a-z0-9]{1,6}$/i)?.[0] ?? "";
      return ext ? `[file${ext}]` : "[file]";
    }
    // Preserve error code class (underscore_separated_codes), not message text
    if (key === "rawError" || key === "error" || key === "normalizedReason") {
      const code = value.toLowerCase().match(/^[a-z0-9_]+/)?.[0] ?? "error";
      return `[${code.slice(0, 32)}]`;
    }
    // Everything else: replace with type descriptor
    return `[string:${Math.min(value.length, 9999)}]`;
  }

  if (Array.isArray(value)) {
    return {
      _type:   "array",
      _length: value.length,
      _sample: value.length > 0 ? scrubValue("item", value[0], depth + 1) : null
    };
  }

  if (typeof value === "object") {
    const out: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      out[k] = scrubValue(k, v, depth + 1);
    }
    return out;
  }

  return typeof value;
}

export function scrubContext(context: Record<string, any>): Record<string, any> {
  const out: Record<string, any> = {};
  for (const [k, v] of Object.entries(context)) {
    out[k] = scrubValue(k, v, 0);
  }
  return out;
}

// ── Outbound field whitelist ───────────────────────────────────────────────────
// Hard enforcement: only these top-level keys ever reach the network.
// Everything else is stripped before the fetch call regardless of scrubber output.

const ROUTE_REQUEST_PERMITTED_KEYS = new Set([
  "hyp", "via", "pathway", "tier", "ts", "clientVer", "contextShape"
]);

function enforceOutboundWhitelist(payload: Record<string, any>): Record<string, any> {
  const out: Record<string, any> = {};
  for (const key of ROUTE_REQUEST_PERMITTED_KEYS) {
    if (key in payload) out[key] = payload[key];
  }
  return out;
}

// ── Network ───────────────────────────────────────────────────────────────────

function getClientVersion(): string {
  try {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    return String(require(CLIENT_VER_PATH)?.version ?? "0.0.0");
  } catch {
    return "0.0.0";
  }
}

function nvUrl(endpoint: string): string {
  // Convert /route → /?path=route, /auth/login → /?path=auth/login
  const clean = endpoint.replace(/^\//, "");
  return `${NV_SERVER}/index.php?path=${encodeURIComponent(clean)}`;
}

function extractJsonObject(rawText: string): any {
  const text = String(rawText || "").trim();
  if (!text) throw new Error("empty_response");

  try { return JSON.parse(text); } catch {}

  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
  if (fenced?.[1]) {
    try { return JSON.parse(fenced[1].trim()); } catch {}
  }

  const first = text.indexOf("{");
  const last = text.lastIndexOf("}");
  if (first >= 0 && last > first) {
    const candidate = text.slice(first, last + 1);
    try { return JSON.parse(candidate); } catch {}
  }

  throw new Error("parse_failed");
}

function normalizeRouteResponse(parsed: any, fallbackVia?: ManifoldVia): any {
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;

  const nested = parsed.route || parsed.package || parsed.data || parsed.result || parsed.response || parsed.RoutePackage;
  const source = nested && typeof nested === "object" && !Array.isArray(nested) ? { ...parsed, ...nested } : parsed;

  const prompt = source.systemPrompt
    ?? source.system_prompt
    ?? source.prompt
    ?? source.content
    ?? source.instructions
    ?? source.message;

  if (!prompt) return source;

  return {
    ...source,
    ok: source.ok !== false,
    via: source.via || fallbackVia,
    systemPrompt: String(prompt),
    strategy: source.strategy ?? source.routeStrategy ?? source.mode ?? "standard",
    threshold: source.threshold ?? source.confidenceThreshold ?? 0.74,
    mode: source.mode ?? source.pathway ?? source.via ?? fallbackVia,
    lane: source.lane ?? source.channel ?? "OTHER",
    meta: source.meta ?? undefined
  };
}

async function postRoute(
  body: Record<string, any>,
  token: string
): Promise<any> {
  const ctrl    = new AbortController();
  const timeout = setTimeout(() => ctrl.abort(), ROUTE_TIMEOUT);
  const url     = nvUrl("/route");

  try {
    const res = await fetch(url, {
      method:  "POST",
      headers: {
        "Content-Type":  "application/json",
        "Authorization": token ? `Bearer ${token}` : "",
        "X-NV-Client":   "verbatim"
      },
      body:   JSON.stringify(enforceOutboundWhitelist(body)),
      signal: ctrl.signal
    });

    const contentType = res.headers.get("content-type") || "";
    const rawText = await res.text().catch(() => "");

    if (!res.ok) {
      return {
        ok: false,
        status: res.status,
        error: "route_http_error",
        contentType,
        rawPreview: rawText.slice(0, 240).replace(/\s+/g, " ").trim()
      };
    }

    try {
      const parsed = extractJsonObject(rawText);
      const normalized = normalizeRouteResponse(parsed, String(body.via || "") as ManifoldVia);
      if (normalized !== parsed) {
        return {
          ...normalized,
          repaired: true,
          repairReason: "normalized_route_response"
        };
      }
      return normalized;
    } catch {
      return {
        ok: false,
        status: res.status,
        error: "parse_failed",
        contentType,
        rawPreview: rawText.slice(0, 240).replace(/\s+/g, " ").trim()
      };
    }
  } catch (err: any) {
    if (err?.name === "AbortError") return { ok: false, error: "timeout" };
    return { ok: false, error: "network_error", detail: err?.message ?? String(err) };
  } finally {
    clearTimeout(timeout);
  }
}
const OFFLINE_FALLBACK_PROMPTS: Record<ManifoldVia, string> = {
  AI1A:          "You are a bounded manifest correction assistant. Return valid JSON only. Correct only the locator or formatting mismatch described. No prose.",
  AI1B:          "You are a bounded manifest correction assistant. Correct the FilePath only. Return valid JSON only. No prose.",
  AI1C:          "You are a bounded manifest correction assistant. Correct the manifest payload structure only. Return valid JSON only. No prose.",
  AI1OTHER:      "You are a manifest diagnostic assistant. Return summary and issues only as valid JSON. No prose.",
  AI2chat:       "You are an execution-first code change assistant. Return valid JSON only. No prose.",
  AI3diagnostic: "You are a post-run diagnostic assistant. Return valid JSON only. No prose."
};

function buildOfflineFallback(via: ManifoldVia): RoutePackage {
  return {
    via,
    systemPrompt: OFFLINE_FALLBACK_PROMPTS[via],
    strategy:     "fallback",
    threshold:    0.74,
    mode:         via,
    lane:         via.replace("AI1", "").replace("AI2", "").replace("AI3", "") || "OTHER",
    offline:      true
  };
}

// ── Core: request a route package ─────────────────────────────────────────────

export async function requestRoutePackage(
  req: ManifoldRequest
): Promise<RoutePackage> {
  const { via, pathway, context, hypToken } = req;
  const token   = getRemoteAccessToken();
  const tier    = getUserTier();
  const scrubbed = scrubContext(context);

  logSystem("Manifold", {
    event:   "route_request",
    via,
    pathway,
    hypToken,
    tier,
    contextKeys: Object.keys(scrubbed)
  });

  const payload = {
    hyp:          hypToken,
    pathway,
    via,
    tier,
    ts:           Date.now(),
    clientVer:    getClientVersion(),
    contextShape: scrubbed
  };

  try {
    const res = await postRoute(payload, token);

    if (res?.ok && res?.systemPrompt) {
      const pkg: RoutePackage = {
        via,
        systemPrompt: String(res.systemPrompt),
        strategy:     String(res.strategy   ?? "standard"),
        threshold:    Number(res.threshold  ?? 0.74),
        mode:         String(res.mode       ?? via),
        lane:         String(res.lane       ?? "OTHER"),
        offline:      false,
        meta:         res.meta ?? undefined
      };
      logSystem("Manifold", {
        event: res.repaired ? "route_received_repaired" : "route_received",
        via,
        strategy: pkg.strategy,
        offline: false,
        repairReason: res.repairReason || ""
      });
      return pkg;
    }

    logSystem("Manifold", {
      event: "route_fallback",
      via,
      reason: res?.error ?? "no_route",
      status: res?.status ?? null,
      contentType: res?.contentType ?? "",
      rawPreview: res?.rawPreview ?? ""
    });
    return buildOfflineFallback(via);

  } catch (err: any) {
    logSystem("Manifold", { event: "route_error", via, error: err?.message ?? String(err) });
    return buildOfflineFallback(via);
  }
}

// ── Telemetry shape relay ─────────────────────────────────────────────────────
// Sends structural tokens only. Never called with real content.

export async function relayTelemetry(tokens: string[]): Promise<void> {
  const token = getRemoteAccessToken();
  const tier  = getUserTier();
  if (!token || !tokens.length) return;

  const ctrl    = new AbortController();
  const timeout = setTimeout(() => ctrl.abort(), ROUTE_TIMEOUT);

  try {
    await fetch(nvUrl("/telemetry"), {
      method:  "POST",
      headers: {
        "Content-Type":  "application/json",
        "Authorization": `Bearer ${token}`,
        "X-NV-Client":   "verbatim"
      },
      body: JSON.stringify(
        enforceOutboundWhitelist({ tier, ts: Date.now(), tokens })
      ),
      signal: ctrl.signal
    });
  } catch {
    // Best effort — never blocks
  } finally {
    clearTimeout(timeout);
  }
}