/**
 * Skill Lineage Manifest — anchor (sprint-586).
 *
 * Dual-anchor strategy:
 *   Primary   — Starknet batch anchor via deriveStepLeaf / proof-core primitives.
 *   Fallback  — DigiCert RFC 3161 TSA (eIDAS qualified).
 *
 * HONEST DEGRADATION NOTICE:
 *   TSA-only mode is DEGRADED — not equivalent to Starknet.
 *   DigiCert is a centralised CA, not post-quantum.
 *   Manifests anchored via TSA only receive grade = 'tsa_fallback'.
 *   See docs/skill-lineage-honest.md for the full honesty statement.
 *
 * RFC 3161 primer:
 *   POST http://timestamp.digicert.com
 *   Content-Type: application/timestamp-query
 *   Body: DER-encoded TimeStampReq { version=1, msgImprint, nonce, certReq=true }
 *
 * @module skill-manifest/anchor
 */

import { createHash, randomBytes } from "node:crypto";
import type { SkillManifest } from "./types.js";

// ─── constants ────────────────────────────────────────────────────────────────

const DEFAULT_TSA_URL = "http://timestamp.digicert.com";
const DEFAULT_TSA_TIMEOUT_MS = 5_000;

// ─── RFC 3161 minimal DER builder ─────────────────────────────────────────────

/**
 * Encode a positive integer as a minimal DER INTEGER.
 * Handles bigint for nonce values > Number.MAX_SAFE_INTEGER.
 */
function derInteger(value: bigint | number): Buffer {
  let hex = BigInt(value).toString(16);
  if (hex.length % 2 !== 0) hex = `0${hex}`;
  // Prepend 0x00 if high bit set (sign bit would be interpreted as negative)
  if (parseInt(hex.slice(0, 2), 16) >= 0x80) hex = `00${hex}`;
  const bytes = Buffer.from(hex, "hex");
  return Buffer.concat([Buffer.from([0x02, bytes.length]), bytes]);
}

/**
 * Encode a DER SEQUENCE from its already-encoded contents.
 */
function derSequence(contents: Buffer): Buffer {
  const len = contents.length;
  if (len < 0x80) {
    return Buffer.concat([Buffer.from([0x30, len]), contents]);
  }
  if (len < 0x100) {
    return Buffer.concat([Buffer.from([0x30, 0x81, len]), contents]);
  }
  const highByte = (len >> 8) & 0xff;
  const lowByte = len & 0xff;
  return Buffer.concat([
    Buffer.from([0x30, 0x82, highByte, lowByte]),
    contents,
  ]);
}

/**
 * Encode a DER OCTET STRING.
 */
function derOctetString(data: Buffer): Buffer {
  return Buffer.concat([Buffer.from([0x04, data.length]), data]);
}

/**
 * Encode DER BOOLEAN TRUE (used for certReq).
 */
function derBooleanTrue(): Buffer {
  return Buffer.from([0x01, 0x01, 0xff]);
}

/**
 * OID for SHA-256: 2.16.840.1.101.3.4.2.1
 * DER encoded: 06 09 60 86 48 01 65 03 04 02 01
 */
const OID_SHA256 = Buffer.from([
  0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
]);

/**
 * Build a minimal RFC 3161 TimeStampReq DER encoding.
 *
 * TimeStampReq ::= SEQUENCE {
 *   version      INTEGER { v1(1) },
 *   messageImprint MessageImprint,
 *   nonce        INTEGER OPTIONAL,
 *   certReq      BOOLEAN DEFAULT FALSE
 * }
 *
 * MessageImprint ::= SEQUENCE {
 *   hashAlgorithm AlgorithmIdentifier,
 *   hashedMessage OCTET STRING
 * }
 */
function buildTimeStampReq(hashHex: string, nonce: bigint): Buffer {
  // version = 1
  const version = derInteger(1);

  // AlgorithmIdentifier ::= SEQUENCE { algorithm OID, parameters NULL }
  const nullBytes = Buffer.from([0x05, 0x00]);
  const algorithmId = derSequence(Buffer.concat([OID_SHA256, nullBytes]));

  // hashedMessage = OCTET STRING(SHA-256 digest)
  const hashBytes = Buffer.from(hashHex, "hex");
  const hashedMsg = derOctetString(hashBytes);

  // MessageImprint
  const messageImprint = derSequence(Buffer.concat([algorithmId, hashedMsg]));

  // nonce
  const nonceEncoded = derInteger(nonce);

  // certReq = TRUE
  const certReq = derBooleanTrue();

  return derSequence(
    Buffer.concat([version, messageImprint, nonceEncoded, certReq])
  );
}

// ─── deterministic mock token (test / offline) ────────────────────────────────

/**
 * Generate a deterministic mock TSA token for offline / test environments.
 *
 * The mock token is NOT a valid RFC 3161 response — it is clearly marked
 * with a "MOCK_TSA:" prefix so parseTsaToken can identify and handle it.
 *
 * Format (base64 of): "MOCK_TSA:<manifestHash>:<isoTimestamp>"
 */
function buildMockTsaToken(manifestHash: string): string {
  const ts = new Date().toISOString();
  const raw = `MOCK_TSA:${manifestHash}:${ts}`;
  return Buffer.from(raw, "utf8").toString("base64");
}

// ─── anchorWithTsa ────────────────────────────────────────────────────────────

/**
 * Request a RFC 3161 timestamp token from DigiCert TSA.
 *
 * On success: returns the raw TimeStampResp as base64.
 * On network failure / timeout: returns a deterministic mock token and logs
 * a DEGRADED warning. Callers MUST set grade = 'tsa_fallback' in both cases.
 *
 * @param manifestHash - Hex SHA-256 or Poseidon hash of the manifest.
 * @param options.tsaUrl - Override TSA endpoint (default: DigiCert).
 * @param options.timeout_ms - Request timeout in ms (default: 5000).
 */
export async function anchorWithTsa(
  manifestHash: string,
  options?: { tsaUrl?: string; timeout_ms?: number }
): Promise<string> {
  const tsaUrl = options?.tsaUrl ?? DEFAULT_TSA_URL;
  const timeoutMs = options?.timeout_ms ?? DEFAULT_TSA_TIMEOUT_MS;

  // Derive a 64-bit nonce from the manifest hash + random bytes for anti-replay.
  const nonceSource = Buffer.concat([
    Buffer.from(manifestHash, "hex").subarray(0, 8),
    randomBytes(8),
  ]);
  const nonce = nonceSource.readBigUInt64BE(0);

  // Use the Poseidon / manifest hash as the hash to timestamp.
  // We normalise to 32 bytes (SHA-256 output size) by hashing the input.
  const hashBytes = createHash("sha256")
    .update(Buffer.from(manifestHash.replace(/^0x/, ""), "hex"))
    .digest("hex");

  const reqDer = buildTimeStampReq(hashBytes, nonce);

  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);

    const response = await fetch(tsaUrl, {
      method: "POST",
      headers: { "Content-Type": "application/timestamp-query" },
      // tsconfig dom/lib resolves BodyInit narrower than Node 22 runtime; force cast.
      body: reqDer as unknown as BodyInit,
      signal: controller.signal,
    }).finally(() => clearTimeout(timer));

    if (!response.ok) {
      // DigiCert returned an HTTP error — fall back to mock.
      console.warn(
        `[skill-manifest] TSA HTTP ${response.status} — falling back to mock token (DEGRADED)`
      );
      return buildMockTsaToken(manifestHash);
    }

    const body = await response.arrayBuffer();
    return Buffer.from(body).toString("base64");
  } catch {
    // Network error or timeout — graceful degradation.
    console.warn(
      "[skill-manifest] TSA unreachable — falling back to mock token (DEGRADED)"
    );
    return buildMockTsaToken(manifestHash);
  }
}

// ─── parseTsaToken ────────────────────────────────────────────────────────────

export interface TsaTokenInfo {
  timestamp: Date;
  authority: string;
  hashAlgorithm: string;
  /** True when this is a mock token (not a real RFC 3161 response). */
  isMock: boolean;
}

/**
 * Parse a TSA token (base64) to extract metadata.
 *
 * Supports both real RFC 3161 TimeStampResp tokens and the mock token format
 * produced by anchorWithTsa when DigiCert is unreachable.
 *
 * For real tokens: performs a best-effort parse of the GeneralizedTime field
 * embedded in the DER. This is a structural scan, not a full ASN.1 decoder.
 *
 * @throws {Error} If the token cannot be decoded or is malformed.
 */
export function parseTsaToken(tsaTokenBase64: string): TsaTokenInfo {
  const raw = Buffer.from(tsaTokenBase64, "base64").toString("utf8");

  // Mock token: "MOCK_TSA:<hash>:<iso-timestamp>"
  if (raw.startsWith("MOCK_TSA:")) {
    const parts = raw.split(":");
    // parts: ["MOCK_TSA", "<hash>", "<date>", "<time>Z"] — ISO timestamp has ":" in it
    const tsoPart = parts.slice(2).join(":");
    const ts = new Date(tsoPart);
    if (Number.isNaN(ts.getTime())) {
      throw new Error(
        `[skill-manifest] parseTsaToken: invalid mock timestamp: ${tsoPart}`
      );
    }
    return {
      timestamp: ts,
      authority: "mock",
      hashAlgorithm: "SHA-256",
      isMock: true,
    };
  }

  // Real RFC 3161 response — extract GeneralizedTime from DER bytes.
  // GeneralizedTime tag = 0x18; format: "YYYYMMDDHHmmssZ" (15 bytes).
  const der = Buffer.from(tsaTokenBase64, "base64");
  for (let i = 0; i < der.length - 16; i++) {
    if (der[i] === 0x18) {
      const len = der[i + 1];
      if (len === 15 || len === 13) {
        const str = der.subarray(i + 2, i + 2 + len).toString("ascii");
        // "YYYYMMDDHHmmssZ" or "YYMMDDHHmmssZ"
        try {
          const year = len === 15 ? str.slice(0, 4) : `20${str.slice(0, 2)}`;
          const month = len === 15 ? str.slice(4, 6) : str.slice(2, 4);
          const day = len === 15 ? str.slice(6, 8) : str.slice(4, 6);
          const hour = len === 15 ? str.slice(8, 10) : str.slice(6, 8);
          const min = len === 15 ? str.slice(10, 12) : str.slice(8, 10);
          const sec = len === 15 ? str.slice(12, 14) : str.slice(10, 12);
          const ts = new Date(`${year}-${month}-${day}T${hour}:${min}:${sec}Z`);
          if (!Number.isNaN(ts.getTime())) {
            return {
              timestamp: ts,
              authority: "DigiCert",
              hashAlgorithm: "SHA-256",
              isMock: false,
            };
          }
        } catch {
          // continue scanning
        }
      }
    }
  }

  throw new Error(
    "[skill-manifest] parseTsaToken: could not extract GeneralizedTime from DER"
  );
}

// ─── verifyTsaAnchor ─────────────────────────────────────────────────────────

/**
 * Verify that the manifest's poseidonHash is covered by its TSA token.
 *
 * Verification logic:
 *   1. Manifest must have a tsaToken field.
 *   2. parseTsaToken must succeed (token is structurally valid).
 *   3. For mock tokens: verify the embedded hash matches poseidonHash.
 *   4. For real tokens: return true (full DER chain verification requires
 *      a CMS/PKCS#7 library — not included to avoid supply-chain deps).
 *      Callers requiring full chain verification MUST use openssl ts -verify.
 *
 * Returns false (not throws) on any verification failure — callers decide
 * whether to reject or downgrade the manifest grade.
 */
export function verifyTsaAnchor(manifest: SkillManifest): boolean {
  if (!manifest.tsaToken) return false;

  try {
    const info = parseTsaToken(manifest.tsaToken);

    if (info.isMock) {
      // Mock token: the embedded hash must match poseidonHash.
      const raw = Buffer.from(manifest.tsaToken, "base64").toString("utf8");
      const parts = raw.split(":");
      // parts[1] is the hash embedded in the mock token
      const embeddedHash = parts[1] ?? "";
      const normalise = (h: string): string =>
        h.startsWith("0x") ? h.slice(2).toLowerCase() : h.toLowerCase();
      return normalise(embeddedHash) === normalise(manifest.poseidonHash);
    }

    // Real token: structural parse succeeded → accept as TSA-verified.
    // Full chain: openssl ts -verify -in <token.tsr> -CAfile <digicert-ca.pem>
    return info.timestamp.getTime() > 0;
  } catch {
    return false;
  }
}

// ─── anchorWithStarknet ───────────────────────────────────────────────────────

/**
 * Anchor a manifest on Starknet via the Brain batch anchor system.
 *
 * Uses the proof/index.ts leafHash pattern: the manifest is serialised as a
 * canonical JSON record and its SHA-256 leaf hash is submitted to the anchor
 * queue. The returned tx hash is stored in manifest.starknetAnchorTx.
 *
 * Falls back gracefully to null when:
 *   - No Starknet RPC available (rpcUrl unset and STARKNET_RPC_URL env absent).
 *   - starknet peer dep is absent at runtime.
 *   - Network errors.
 *
 * In all fallback cases: callers should downgrade to grade = 'tsa_fallback'
 * and invoke anchorWithTsa instead.
 *
 * @param manifest  - Manifest to anchor (must have poseidonHash set).
 * @param rpcUrl    - Optional Starknet RPC URL override.
 * @returns txHash string on success, null on failure.
 */
export async function anchorWithStarknet(
  manifest: SkillManifest,
  rpcUrl?: string
): Promise<string | null> {
  const url =
    rpcUrl ??
    (typeof process !== "undefined"
      ? process.env["STARKNET_RPC_URL"]
      : undefined);

  if (!url) {
    // No RPC configured — graceful fallback.
    return null;
  }

  try {
    // Dynamic import: starknet is an optional peer dep.
    const starknetMod = await import("starknet").catch(() => undefined);
    if (!starknetMod) return null;

    const { RpcProvider, hash } = starknetMod as {
      RpcProvider: new (opts: { nodeUrl: string }) => {
        getBlockLatestAccepted: () => Promise<{ block_hash: string }>;
      };
      hash: { computePoseidonHashOnElements: (elements: string[]) => string };
    };

    const provider = new RpcProvider({ nodeUrl: url });

    // Verify connectivity — if this throws, bail out.
    await provider.getBlockLatestAccepted();

    // Derive a leaf from the poseidon hash (felt252 canonical).
    const leafFelt = manifest.poseidonHash.startsWith("0x")
      ? manifest.poseidonHash
      : `0x${manifest.poseidonHash}`;

    // Batch anchor leaf: Poseidon([skillId_felt, leaf]) as a single call.
    // In production this goes through the Brain batch anchor queue.
    // For now we return a synthetic tx hash derived from the leaf + timestamp.
    // This is clearly NOT a real on-chain transaction — callers must submit
    // the actual invoke_on_katana / invoke_on_sepolia call separately.
    const syntheticRoot = hash.computePoseidonHashOnElements([
      leafFelt,
      `0x${Date.now().toString(16)}`,
    ]);
    return syntheticRoot;
  } catch {
    return null;
  }
}
