/**
 * Covenant agent tokens — HMAC-signed payloads for POST /api/tool-proxy
 * Operator guide: docs/GATEWAY-COVENANT-C1.md, docs/GATEWAY-COVENANT-C3.md
 */

import { env } from '@4runr/shared';
import crypto from 'crypto';

/** Must match startup validation in `middleware/security.ts` */
export const DEFAULT_DEV_SIGNING_SECRET = 'default-secret-change-in-production';

export const MIN_TOKEN_TTL_HOURS = 1;
export const MAX_TOKEN_TTL_HOURS = 168;

const INSECURE_SECRET_SUBSTRINGS = [
  'secret',
  'change-me',
  'change-in-production',
  'test-secret',
  'default-dev',
  'jwt-secret',
];

export interface AgentTokenData {
  agent_id: string;
  tools: string[];
  /** Stored in token for future enforcement; not checked on proxy today — see C1/C3 docs */
  permissions: string[];
  expires_at: string;
}

export function looksInsecureSecret(value: string): boolean {
  const lower = value.toLowerCase();
  return INSECURE_SECRET_SUBSTRINGS.some((p) => lower.includes(p));
}

/** Production signing rules (startup + runtime). Returns error message or null if OK. */
export function signingSecretProductionError(secret: string | undefined): string | null {
  if (!secret || secret.length < 32) {
    return 'SIGNING_SECRET must be set to at least 32 characters in production (Covenant / agent tokens)';
  }
  if (secret === DEFAULT_DEV_SIGNING_SECRET || looksInsecureSecret(secret)) {
    return 'SIGNING_SECRET must not use a default or obvious placeholder in production';
  }
  return null;
}

function isProductionNode(): boolean {
  return (process.env['NODE_ENV'] ?? env.NODE_ENV) === 'production';
}

export function getSigningSecret(): { secret: string; error?: string } {
  const s = process.env['SIGNING_SECRET'];
  if (isProductionNode()) {
    const err = signingSecretProductionError(s);
    if (err) {
      return { secret: '', error: err };
    }
    return { secret: s! };
  }
  return { secret: s && s.length >= 32 ? s : DEFAULT_DEV_SIGNING_SECRET };
}

function hmacSignaturesMatch(providedHex: string, expectedHex: string): boolean {
  if (typeof providedHex !== 'string' || typeof expectedHex !== 'string') {
    return false;
  }
  const provided = Buffer.from(providedHex, 'hex');
  const expected = Buffer.from(expectedHex, 'hex');
  if (provided.length !== expected.length || provided.length === 0) {
    return false;
  }
  return crypto.timingSafeEqual(provided, expected);
}

function parseTokenExpiry(expiresAtRaw: string): Date | null {
  const expiresAt = new Date(expiresAtRaw);
  if (Number.isNaN(expiresAt.getTime())) {
    return null;
  }
  return expiresAt;
}

export type MintRequestInput = {
  agent_id?: unknown;
  tools?: unknown;
  permissions?: unknown;
  expiresInHours?: unknown;
};

export function validateMintRequest(
  body: MintRequestInput
):
  | {
      ok: true;
      data: {
        agent_id: string;
        tools: string[];
        permissions: string[];
        expiresInHours: number;
      };
    }
  | { ok: false; error: string; details?: Record<string, unknown> } {
  const agent_id = body.agent_id;
  if (typeof agent_id !== 'string' || !agent_id.trim()) {
    return { ok: false, error: 'agent_id must be a non-empty string' };
  }

  if (!isNonEmptyStringArray(body.tools)) {
    return {
      ok: false,
      error: 'tools must be a non-empty array of non-empty strings',
      details: { minTools: 1 },
    };
  }

  if (!isStringArray(body.permissions)) {
    return {
      ok: false,
      error: 'permissions must be an array of strings',
    };
  }

  const rawHours = body.expiresInHours ?? 24;
  if (typeof rawHours !== 'number' || !Number.isFinite(rawHours)) {
    return {
      ok: false,
      error: 'expiresInHours must be a number',
    };
  }
  const expiresInHours = Math.floor(rawHours);
  if (expiresInHours < MIN_TOKEN_TTL_HOURS || expiresInHours > MAX_TOKEN_TTL_HOURS) {
    return {
      ok: false,
      error: `expiresInHours must be between ${MIN_TOKEN_TTL_HOURS} and ${MAX_TOKEN_TTL_HOURS}`,
      details: { min: MIN_TOKEN_TTL_HOURS, max: MAX_TOKEN_TTL_HOURS },
    };
  }

  return {
    ok: true,
    data: {
      agent_id: agent_id.trim(),
      tools: body.tools.map((t) => t.trim()),
      permissions: body.permissions.map((p) => String(p).trim()),
      expiresInHours,
    },
  };
}

function isNonEmptyStringArray(val: unknown): val is string[] {
  return (
    Array.isArray(val) &&
    val.length > 0 &&
    val.every((x) => typeof x === 'string' && x.trim().length > 0)
  );
}

function isStringArray(val: unknown): val is string[] {
  return Array.isArray(val) && val.every((x) => typeof x === 'string');
}

/**
 * Mint `{base64Payload}.{hexSignature}` for tests and generate-agent-token route.
 */
export function signAgentToken(
  payload: AgentTokenData,
  secretOverride?: string
): { token: string } | { error: string } {
  const { secret: fromEnv, error: secretError } = getSigningSecret();
  const secret = secretOverride ?? fromEnv;
  if (!secret) {
    return { error: secretError || 'Signing is not configured' };
  }

  const encryptedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
  const signature = crypto
    .createHmac('sha256', secret)
    .update(encryptedPayload)
    .digest('hex');

  return { token: `${encryptedPayload}.${signature}` };
}

/**
 * Verify HMAC, parse payload, enforce expiry.
 */
export function verifyAgentToken(
  agentToken: string
): { valid: boolean; data?: AgentTokenData; error?: string } {
  const { secret, error: secretError } = getSigningSecret();
  if (!secret) {
    return { valid: false, error: secretError || 'Signing is not configured' };
  }

  try {
    const [encryptedPayload, signature] = agentToken.split('.');

    if (!encryptedPayload || !signature) {
      return { valid: false, error: 'Invalid token format' };
    }

    const validSignature = crypto
      .createHmac('sha256', secret)
      .update(encryptedPayload)
      .digest('hex');

    if (!hmacSignaturesMatch(signature, validSignature)) {
      return { valid: false, error: 'Invalid signature' };
    }

    const payload = Buffer.from(encryptedPayload, 'base64').toString('utf8');
    const tokenData: AgentTokenData = JSON.parse(payload);

    const expiresAt = parseTokenExpiry(tokenData.expires_at);
    if (!expiresAt) {
      return { valid: false, error: 'Invalid token expiry' };
    }
    if (expiresAt.getTime() <= Date.now()) {
      return { valid: false, error: 'Token expired' };
    }

    return { valid: true, data: tokenData };
  } catch {
    return { valid: false, error: 'Token verification failed' };
  }
}

/**
 * Enforce tool allowlist from verified token payload.
 */
export function assertToolAllowed(
  tokenData: AgentTokenData,
  tool: string
): { allowed: true } | { allowed: false; allowedTools: string[] } {
  if (!tokenData.tools.includes(tool)) {
    return { allowed: false, allowedTools: tokenData.tools };
  }
  return { allowed: true };
}
