// Copyright 2026 Cisco Systems, Inc. and its affiliates
// SPDX-License-Identifier: Apache-2.0
//
// Browser-side OPA evaluator. Lazy-loads compiled WASM modules from
// /opa/<domain>.wasm (generated by scripts/build-policy-assets.ts) and
// exposes a single evalDomain() the Live Test pane calls on every
// state change. Each domain's loaded policy is cached so we only pay
// the WASM instantiation cost once per page load.
//
// We deliberately keep this module client-only (no SSR) and use
// dynamic import() so the @open-policy-agent/opa-wasm bundle isn't
// included in the docs page until the operator opens the creator.

import type { OpaManifest, OpaResult } from '../types';

// Next.js basePath ("" or "/defenseclaw") gets baked into client bundles
// via env.NEXT_PUBLIC_BASE_PATH in next.config — see how it's set there.
// Using it here keeps the fetch URLs honest under GitHub Pages hosting.
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? '';

function asset(path: string): string {
  return `${BASE_PATH}${path}`;
}

let manifestPromise: Promise<OpaManifest | null> | null = null;
const policyCache = new Map<string, Promise<unknown>>();

async function loadManifest(): Promise<OpaManifest | null> {
  if (manifestPromise) return manifestPromise;
  manifestPromise = (async () => {
    try {
      const res = await fetch(asset('/opa/manifest.json'), { cache: 'force-cache' });
      if (!res.ok) return null;
      return (await res.json()) as OpaManifest;
    } catch {
      return null;
    }
  })();
  return manifestPromise;
}

async function loadPolicyForDomain(domain: string): Promise<unknown> {
  const cached = policyCache.get(domain);
  if (cached) return cached;
  const promise = (async () => {
    const manifest = await loadManifest();
    const entry = manifest?.domains.find((d) => d.name === domain);
    if (!entry) {
      throw new OpaUnavailableError(
        `WASM module for domain "${domain}" is not registered. Run \`npm run build:policy-assets\`.`,
      );
    }
    // entry.wasm in the manifest is a root-relative path ("/opa/foo.wasm"),
    // so we must re-prefix with the basePath here too.
    const wasmRes = await fetch(asset(entry.wasm), { cache: 'force-cache' });
    if (!wasmRes.ok) {
      throw new OpaUnavailableError(
        `Failed to fetch ${entry.wasm} (HTTP ${wasmRes.status}).`,
      );
    }
    const bytes = await wasmRes.arrayBuffer();
    // Lazy-load opa-wasm so the bundler keeps it out of the initial
    // docs payload — operators who never open the policy creator never
    // pay for the runtime.
    const { loadPolicy } = await import('@open-policy-agent/opa-wasm');
    return loadPolicy(bytes);
  })();
  policyCache.set(domain, promise);
  return promise;
}

export class OpaUnavailableError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'OpaUnavailableError';
  }
}

interface PolicyHandle {
  setData(data: unknown): void;
  evaluate(input: unknown, entrypoint: string | number): Array<{ result: unknown }>;
}

/**
 * Evaluate a Rego entrypoint against the supplied {input, data} pair.
 * Returns null when the entrypoint is undefined under the policy
 * (OPA convention) — callers should fall back to the rule's default.
 */
export async function evalEntrypoint(
  domain: string,
  entrypoint: string,
  input: unknown,
  data: unknown,
): Promise<unknown | null> {
  const policy = (await loadPolicyForDomain(domain)) as PolicyHandle;
  policy.setData(data);
  const out = policy.evaluate(input, entrypoint);
  if (!Array.isArray(out) || out.length === 0) return null;
  return out[0]?.result ?? null;
}

/**
 * Convenience wrapper: evaluates the conventional verdict + reason
 * pair for a domain. Returns a structured OpaResult the Live Test
 * pane can render directly.
 */
export async function evalDomain(
  domain: string,
  input: unknown,
  data: unknown,
): Promise<OpaResult> {
  const verdictKey = pickVerdictEntrypoint(domain);
  const reasonKey = pickReasonEntrypoint(domain);
  const [verdict, reason] = await Promise.all([
    evalEntrypoint(domain, verdictKey, input, data),
    reasonKey ? evalEntrypoint(domain, reasonKey, input, data) : Promise.resolve(null),
  ]);
  return {
    verdict: verdict == null ? '(undefined)' : String(verdict),
    reason: reason == null ? undefined : String(reason),
    raw: { verdict, reason },
  };
}

function pickVerdictEntrypoint(domain: string): string {
  switch (domain) {
    case 'admission':
      return 'defenseclaw/admission/verdict';
    case 'guardrail':
      return 'defenseclaw/guardrail/severity';
    case 'firewall':
      return 'defenseclaw/firewall/action';
    case 'audit':
      return 'defenseclaw/audit/retain';
    case 'skill_actions':
      return 'defenseclaw/skill_actions/runtime_action';
    default:
      return `defenseclaw/${domain}/verdict`;
  }
}

function pickReasonEntrypoint(domain: string): string | null {
  switch (domain) {
    case 'admission':
      return 'defenseclaw/admission/reason';
    case 'guardrail':
      return 'defenseclaw/guardrail/reason';
    case 'firewall':
      return 'defenseclaw/firewall/rule_name';
    case 'audit':
      return 'defenseclaw/audit/retain_reason';
    case 'skill_actions':
      return null;
    default:
      return null;
  }
}

/** Verdict label → semantic color bucket consumed by VerdictBadge. */
export function verdictTone(
  verdict: string,
): 'positive' | 'caution' | 'negative' | 'neutral' {
  switch (verdict) {
    case 'allowed':
    case 'allow':
    case 'clean':
    case 'true':
      return 'positive';
    case 'warning':
    case 'alert':
    case 'scan':
      return 'caution';
    case 'blocked':
    case 'rejected':
    case 'block':
    case 'deny':
    case 'false':
      return 'negative';
    default:
      return 'neutral';
  }
}

export async function isOpaAvailable(): Promise<boolean> {
  const m = await loadManifest();
  return !!m && m.domains.length > 0;
}
