// hypauthcall.ts
// Scope: client-server translation layer for Nodarama Verbatim.
// The only file in the client that talks outbound to the Nodarama server.
//
// Hypauthcall = Hypothetical Auth Call
// Every call sends a hypothetical manifest — a structural token describing the
// shape of what happened, not the content. No code, file names, or project data
// ever leaves the machine. The server reads the shape and returns a route.
//
// Three trigger types:
//   error          — recorder failed, correction route requested
//   chat           — user sent a chat message, engagement route requested
//   batch_complete — a batch of manifests finished, diagnostic route requested

import fs from "node:fs";
import path from "node:path";
import { getUser, getRemoteAccessToken, getRemoteRefreshToken, isAuthenticated, getUserTier, establishSession, clearSession, type UserTier, type UserRecord } from "./auth";
import { logSystem } from "../playbackCore";

const ROOT = process.cwd();

// ── Server config ─────────────────────────────────────────────────────────────
// The Nodarama auth and routing server.
// URL is the only hardcoded outbound destination in the client.

const NV_SERVER = "https://api.nodarama.com";
function nvUrl(endpoint: string): string {
  const clean = endpoint.replace(/^\//, "");
  return `${NV_SERVER}/index.php?path=${encodeURIComponent(clean)}`;
}
const PING_TIMEOUT_MS = 8000;

// ── Trigger types ─────────────────────────────────────────────────────────────

export type HypTrigger = "error" | "chat" | "batch_complete";

// ── Roman Alphanumeric token ──────────────────────────────────────────────────
// Structural token encoding trigger shape without content.
// Format: <trigger_code><action_class><depth_marker><instance_id>
//
// Trigger codes:
//   E  = error
//   C  = chat
//   B  = batch_complete
//
// Action class (what kind of manifest was involved):
//   I   = insert type
//   II  = replace type
//   III = new/rewrite type
//   IV  = structural (block/function)
//   V   = composite (multiple actions in batch)
//
// Depth marker: single digit 1-9, how many items were in scope
// Instance ID: 4-char alphanumeric, randomized — not tied to any real ID

const TRIGGER_CODE: Record<HypTrigger, string> = {
  error:          "E",
  chat:           "C",
  batch_complete: "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(length: number): string {
  const chars = "ABCDEFGHJKLMNPQRSTVWXYZ23456789"; // no ambiguous chars
  return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
}

function buildHypToken(trigger: HypTrigger, actionType: string, itemCount: number): string {
  const t  = TRIGGER_CODE[trigger];
  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}`;
}

// ── Network helpers ───────────────────────────────────────────────────────────

async function post(endpoint: string, body: Record<string, any>, token: string): Promise<any> {
  const ctrl    = new AbortController();
  const timeout = setTimeout(() => ctrl.abort(), PING_TIMEOUT_MS);
  const url     = nvUrl(endpoint);

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

    const rawText = await res.text().catch(() => "");
    let data: Record<string, any> = {};
    let parsedJson = false;

    if (rawText.trim()) {
      try {
        data = JSON.parse(rawText);
        parsedJson = true;
      } catch {
        data = { rawBody: rawText.slice(0, 500) };
      }
    }

    const remoteError = String(data.error || data.message || "").trim();
    const failureType = res.ok
      ? (parsedJson ? "none" : "parse_failed")
      : res.status === 400 ? "bad_request"
      : res.status === 401 || res.status === 403 ? "auth_rejected"
      : res.status === 404 ? "remote_endpoint_not_found"
      : res.status === 409 ? "already_exists"
      : res.status >= 500 ? "remote_server_error"
      : "remote_request_failed";

    if (!res.ok) {
      return {
        ok: false,
        connected: true,
        status: res.status,
        endpoint,
        url,
        parsedJson,
        failureType,
        remoteError: remoteError || failureType,
        error: remoteError || failureType,
        ...data
      };
    }

    return {
      connected: true,
      status: res.status,
      endpoint,
      url,
      parsedJson,
      failureType,
      ...data
    };
  } catch (err: any) {
    const timedOut = err?.name === "AbortError";
    return {
      ok: false,
      connected: false,
      status: 0,
      endpoint,
      url,
      parsedJson: false,
      failureType: timedOut ? "timeout" : "network_error",
      remoteError: timedOut ? "timeout" : (err?.message ?? "network_error"),
      error: timedOut ? "timeout" : (err?.message ?? "network_error")
    };
  } finally {
    clearTimeout(timeout);
  }
}
export async function bootAuth(): Promise<BootAuthResult> {
  const refreshToken = getRemoteRefreshToken();

  if (!refreshToken) {
    logSystem("Hypauthcall", { event: "boot_auth", result: "unregistered", tier: "limited" });
    return { ok: true, tier: "limited", serverUp: false, message: "unregistered_limited_access" };
  }

  const user = getUser();

  try {
    const res = await post("/auth/refresh", {
      refreshToken,
      clientVer: getClientVersion()
    }, "");

    if (res.ok && res.user) {
      const nextTier = (res.user.accessTier ?? res.user.tier ?? res.user.access ?? res.access ?? user.tier ?? "limited") as UserTier;

      establishSession({
        userId:             String(res.user.id ?? user.userId ?? ""),
        email:              String(res.user.email ?? user.email ?? ""),
        tier:               nextTier,
        licenseKey:         user.licenseKey,
        telemetry:          user.telemetry,
        remoteAccessToken:  String(res.accessToken ?? ""),
        remoteRefreshToken: String(res.refreshToken ?? refreshToken)
      });

      logSystem("Hypauthcall", { event: "boot_auth", result: "refreshed", tier: nextTier });
      return { ok: true, tier: nextTier, serverUp: true, message: "refreshed" };
    }

    if (res.status === 401 || res.error === "invalid_refresh_token" || res.error === "expired_refresh_token") {
      clearSession();
      logSystem("Hypauthcall", { event: "boot_auth", result: "rejected", reason: res.error ?? "invalid_refresh_token" });
      return { ok: false, tier: "limited", serverUp: true, message: res.error ?? "auth_rejected" };
    }

    logSystem("Hypauthcall", { event: "boot_auth", result: "server_error", status: res.status });
    return { ok: true, tier: user.tier, serverUp: true, message: "server_error_using_cache" };
  } catch {
    if (isAuthenticated()) {
      logSystem("Hypauthcall", { event: "boot_auth", result: "offline", tier: user.tier });
      return { ok: true, tier: user.tier, serverUp: false, message: "server_unreachable_using_cache" };
    }
    logSystem("Hypauthcall", { event: "boot_auth", result: "offline_unauth", tier: "limited" });
    return { ok: true, tier: "limited", serverUp: false, message: "server_unreachable_limited_access" };
  }
}

// ── Login — called from deck /auth/login endpoint ─────────────────────────────

export type LoginResult = {
  ok:      boolean;
  user?:   UserRecord;
  error?:  string;
};

export async function login(email: string, password: string): Promise<LoginResult> {
  try {
    const res = await post("/auth/login", {
      email,
      password,
      clientVer: getClientVersion()
    }, "");

    if (!res.ok || !res.user) {
      const error = String(res.remoteError || res.error || "login_failed");
      logSystem("Hypauthcall", {
        event: "login_failed",
        connected: Boolean(res.connected),
        status: res.status ?? null,
        failureType: res.failureType || "unknown",
        remoteError: error,
        endpoint: res.endpoint || "/auth/login"
      });
      return {
        ok: false,
        error,
        message: String(res.message || error),
        failureType: res.failureType || "login_failed",
        status: res.status,
        connected: Boolean(res.connected)
      } as LoginResult;
    }

    const nextTier = (res.user.accessTier ?? res.user.tier ?? res.user.access ?? res.access ?? "limited") as UserTier;

    const user = establishSession({
      userId:             String(res.user.id ?? ""),
      email:              String(res.user.email ?? email),
      tier:               nextTier,
      licenseKey:         "",
      telemetry:          { logs: nextTier === "limited" },
      remoteAccessToken:  String(res.accessToken ?? ""),
      remoteRefreshToken: String(res.refreshToken ?? "")
    });

    logSystem("Hypauthcall", { event: "login", userId: res.user.id, tier: nextTier });
    return { ok: true, user };
  } catch (err: any) {
    const error = err?.message ?? "network_error";
    logSystem("Hypauthcall", { event: "login_failed", connected: false, failureType: "local_exception", remoteError: error });
    return { ok: false, error, failureType: "local_exception", connected: false } as LoginResult;
  }
}
export async function register(email: string, password: string, options?: {
  deviceName?: string;
  deviceFingerprint?: string;
}): Promise<RegisterResult> {
  try {
    const res = await post("/auth/register", {
      email,
      password,
      deviceName: options?.deviceName || "NV Station",
      deviceFingerprint: options?.deviceFingerprint || "",
      clientVer: getClientVersion()
    }, "");

    if (res.ok && res.user) {
      const nextTier = (res.user.accessTier ?? res.user.tier ?? res.user.access ?? res.access ?? "limited") as UserTier;
      const user = establishSession({
        userId:             String(res.user.id ?? ""),
        email:              String(res.user.email ?? email),
        tier:               nextTier,
        licenseKey:         "",
        telemetry:          { logs: nextTier === "limited" },
        remoteAccessToken:  String(res.accessToken ?? ""),
        remoteRefreshToken: String(res.refreshToken ?? "")
      });
      logSystem("Hypauthcall", { event: "register", userId: res.user.id, tier: nextTier });
      return { ok: true, user };
    }

    if (res.activationRequired) {
      return {
        ok: true,
        activationRequired: true,
        activationId: String(res.activationId ?? ""),
        activationUrl: String(res.activationUrl ?? ""),
        message: String(res.message ?? "Activation pending"),
        tier: String(res.accessTier ?? res.tier ?? "limited")
      };
    }

    const error = String(res.remoteError || res.error || res.message || "register_failed");
    logSystem("Hypauthcall", {
      event: "register_failed",
      connected: Boolean(res.connected),
      status: res.status ?? null,
      failureType: res.failureType || "unknown",
      remoteError: error,
      endpoint: res.endpoint || "/auth/register"
    });

    return {
      ok: false,
      error,
      message: String(res.message || error),
      failureType: res.failureType || "register_failed",
      status: res.status,
      connected: Boolean(res.connected)
    } as RegisterResult;
  } catch (err: any) {
    const error = err?.message ?? "network_error";
    logSystem("Hypauthcall", { event: "register_failed", connected: false, failureType: "local_exception", remoteError: error });
    return { ok: false, error, failureType: "local_exception", connected: false } as RegisterResult;
  }
}
export async function checkActivation(activationId: string, options?: {
  deviceName?: string;
  deviceFingerprint?: string;
}): Promise<ActivationStatusResult> {
  try {
    const res = await post("/auth/activation-status", {
      activationId,
      deviceName: options?.deviceName || "NV Station",
      deviceFingerprint: options?.deviceFingerprint || "",
      clientVer: getClientVersion()
    }, "");

    if (res.ok && res.active && res.user) {
      const nextTier = (res.user.accessTier ?? res.user.tier ?? res.user.access ?? res.access ?? "limited") as UserTier;
      const user = establishSession({
        userId:             String(res.user.id ?? ""),
        email:              String(res.user.email ?? ""),
        tier:               nextTier,
        licenseKey:         "",
        telemetry:          { logs: nextTier === "limited" },
        remoteAccessToken:  String(res.accessToken ?? ""),
        remoteRefreshToken: String(res.refreshToken ?? "")
      });
      logSystem("Hypauthcall", { event: "activation_complete", userId: res.user.id, tier: nextTier });
      return { ok: true, active: true, user, tier: nextTier };
    }

    return {
      ok: true,
      active: false,
      message: String(res.message ?? "Activation pending"),
      tier: String(res.accessTier ?? res.tier ?? "limited")
    };
  } catch (err: any) {
    return { ok: false, error: err?.message ?? "network_error" };
  }
}

// ── Route request — the core hypauthcall ─────────────────────────────────────
// Sends a hypothetical manifest token, receives a route instruction.
// No content, no file names, no project data ever sent.

export type RouteInstruction = {
  ok:           boolean;
  route:        string;   // opaque route key — interpreted by EQ call coordinator
  strategy?:    string;   // prompt strategy hint
  threshold?:   number;   // confidence threshold override
  offline?:     boolean;  // true = server unreachable, use local defaults
};

export async function requestRoute(params: {
  trigger:    HypTrigger;
  actionType: string;
  itemCount:  number;
}): Promise<RouteInstruction> {
  const token = getRemoteAccessToken();
  const tier  = getUserTier();

  // Build hypothetical manifest token — anonymized structural description
  const hypToken = buildHypToken(params.trigger, params.actionType, params.itemCount);

  logSystem("Hypauthcall", {
    event:    "route_request",
    trigger:  params.trigger,
    token:    hypToken,   // only the token is logged, never the real content
    tier
  });

  try {
    const res = await post("/route", {
      hyp:   hypToken,
      tier,
      ts:    Date.now()
    }, token);

    if (res.ok && res.route) {
      return {
        ok:        true,
        route:     String(res.route),
        strategy:  res.strategy  ? String(res.strategy)  : undefined,
        threshold: res.threshold ? Number(res.threshold) : undefined
      };
    }

    // Server responded but no valid route — use local default
    return { ok: true, route: "local_default", offline: false };

  } catch {
    // Server unreachable — signal caller to use local defaults
    logSystem("Hypauthcall", { event: "route_request_offline", trigger: params.trigger });
    return { ok: true, route: "local_default", offline: true };
  }
}


export function setRuntimeUserTier(tier: UserTier): void {
  const user = readUser();
  const accessTier = normalizeAccessTier(tier);
  writeUser({
    ...user,
    tier: accessTier,
    licensed: accessTier !== "limited",
    lastAuthAt: Date.now()
  });
}


// ── Run authorization — server gate for imported/automated/external sources ──

export type RunAuthorizationResult = {
  ok: boolean;
  allowed: boolean;
  source?: string;
  reason?: string;
  message?: string;
  accessTier?: string;
  policyVersion?: string;
  connected?: boolean;
  status?: number;
};

export async function authorizeRun(params: {
  source: string;
  action?: string;
  task?: string;
  clientPolicyVersion?: string;
}): Promise<RunAuthorizationResult> {
  const token = getRemoteAccessToken();
  const tier = getUserTier();
  const source = String(params.source || "").trim() || "unknown";

  logSystem("Hypauthcall", {
    event: "authorize_run_request",
    source,
    action: params.action || "",
    tier
  });

  try {
    const res = await post("/auth/authorize-run", {
      source,
      action: String(params.action || ""),
      taskClass: String(params.task || ""),
      tier,
      clientVer: getClientVersion(),
      clientPolicyVersion: String(params.clientPolicyVersion || "run-authority-v1"),
      ts: Date.now()
    }, token);

    if (!res.ok) {
      return {
        ok: false,
        allowed: false,
        connected: Boolean(res.connected),
        status: Number(res.status || 0),
        source,
        reason: String(res.error || res.reason || "run_authorization_failed"),
        message: String(res.message || res.error || "Run authorization failed"),
        accessTier: String(res.accessTier || tier),
        policyVersion: String(res.policyVersion || "run-authority-v1")
      };
    }

    return {
      ok: true,
      allowed: Boolean(res.allowed),
      connected: Boolean(res.connected),
      status: Number(res.status || 200),
      source,
      reason: String(res.reason || ""),
      message: String(res.message || (res.allowed ? "Allowed" : "Run blocked")),
      accessTier: String(res.accessTier || tier),
      policyVersion: String(res.policyVersion || "run-authority-v1")
    };
  } catch (err: any) {
    return {
      ok: false,
      allowed: false,
      connected: false,
      status: 0,
      source,
      reason: "server_unreachable",
      message: err?.message || "Server authorization unavailable",
      accessTier: tier,
      policyVersion: "run-authority-v1"
    };
  }
}

// ── Telemetry upload — structural tokens only, or logs if consented ───────────

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

  const consent = user.telemetry;
  if (!consent.structural) return; // structural consent is always true, but guard anyway

  try {
    await post("/telemetry", {
      tokens,
      tier:      user.tier,
      structural: true,
      ts:        Date.now()
    }, token);
  } catch {
    // Best effort — telemetry failure never blocks the user
  }
}

// ── Phase 6 authority, update, and integrity calls ───────────────────────────

export async function requestAuthorityPacket(): Promise<any> {
  const token = getRemoteAccessToken();
  const tier = getUserTier();
  const res = await post("/system/authority", {
    tier,
    clientVer: getClientVersion(),
    ts: Date.now()
  }, token);
  return res;
}

export async function requestSystemManifest(): Promise<any> {
  const token = getRemoteAccessToken();
  const res = await post("/system/manifest", {
    clientVer: getClientVersion(),
    ts: Date.now()
  }, token);
  return res;
}

export async function requestUpdateManifest(channel = "stable"): Promise<any> {
  const token = getRemoteAccessToken();
  const res = await post("/update/manifest", {
    clientVer: getClientVersion(),
    channel,
    ts: Date.now()
  }, token);
  return res;
}

export async function reportSystemIntegrity(report: any): Promise<any> {
  const token = getRemoteAccessToken();
  const res = await post("/system/tamper-report", {
    ...report,
    clientVer: getClientVersion(),
    ts: Date.now()
  }, token);
  return res;
}

// ── Client version ────────────────────────────────────────────────────────────

function getClientVersion(): string {
  try {
    const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
    return String(pkg?.version ?? "0.0.0");
  } catch {
    return "0.0.0";
  }
}

// ── Session keepalive — called periodically by deck ───────────────────────────

export async function keepalive(): Promise<void> {
  const token = getRemoteAccessToken();
  if (!token) return;
  try {
    await post("/auth/keepalive", { ts: Date.now() }, token);
  } catch {
    // Best effort — keepalive failure does not log out the user
  }
}