/**
 * lib/compliance-audit.js — INSERT-only compliance audit writer.
 *
 * This module is the authoritative writer for identity- and artifact-security-
 * critical events. It is structurally distinct from the legacy appendAudit()
 * in lib/runtime.js:
 *   - appendAudit() is for operational events (agent runs, tool calls, etc.)
 *   - appendComplianceAudit() is for compliance events (artifact reads, shares,
 *     member changes, session events, etc.)
 *
 * APPEND-ONLY CONTRACT:
 *   This module contains no UPDATE or DELETE path. The audit_logs table also
 *   carries database-level triggers (audit_logs_no_update, audit_logs_no_delete)
 *   that prevent mutation at the SQLite engine level. Both layers enforce
 *   immutability independently.
 */

import { getDb, nowIso, newId } from './db.js';

// ── Action vocabulary constants ───────────────────────────────────────────────
// Use these constants instead of raw strings to prevent typos and make
// refactoring safe. Callers: `import { ComplianceActions } from './compliance-audit.js'`

export const ComplianceActions = Object.freeze({
  // Artifact access
  ARTIFACT_READ:              'artifact.read',
  ARTIFACT_READ_DENIED:       'artifact.read.denied',
  ARTIFACT_WRITE:             'artifact.write',
  ARTIFACT_WRITE_DENIED:      'artifact.write.denied',
  ARTIFACT_SHARE_LINK_CREATED:'artifact.share.link.created',
  ARTIFACT_SHARE_LINK_REVOKED:'artifact.share.link.revoked',
  ARTIFACT_SHARE_LINK_ACCESSED:'artifact.share.link.accessed',
  ARTIFACT_ACL_UPDATED:       'artifact.acl.updated',
  ARTIFACT_EXPORTED:          'artifact.exported',
  ARTIFACT_DELETED:           'artifact.deleted',

  // Share link OTP
  SHARE_OTP_SENT:             'share.otp.sent',
  SHARE_OTP_VERIFIED:         'share.otp.verified',
  SHARE_OTP_FAILED:           'share.otp.failed',
  SHARE_OTP_LOCKED_OUT:       'share.otp.locked_out',

  // Space share links (whole-space public links)
  SPACE_SHARE_LINK_CREATED:   'space.share.link.created',
  SPACE_SHARE_LINK_REVOKED:   'space.share.link.revoked',
  SPACE_SHARE_LINK_ACCESSED:  'space.share.link.accessed',

  // Org management
  ORG_MEMBER_INVITED:         'org.member.invited',
  ORG_MEMBER_REMOVED:         'org.member.removed',
  ORG_MEMBER_ROLE_CHANGED:    'org.member.role.changed',
  ORG_MFA_POLICY_CHANGED:     'org.mfa_policy.changed',
  ORG_IP_ALLOWLIST_CHANGED:   'org.ip_allowlist.changed',

  // Session events
  SESSION_SIGNED_IN:          'session.signed_in',
  SESSION_SIGNED_OUT:         'session.signed_out',
  SESSION_MFA_REQUIRED:       'session.mfa_required',
  SESSION_IP_DENIED:          'session.ip_denied',
  AUTH_MEMBERSHIP_REVOKED:    'auth.membership.revoked_detected',

  // Tool events
  TOOL_IRREVERSIBLE_APPROVED: 'tool.irreversible.approved',
  TOOL_IRREVERSIBLE_DENIED:   'tool.irreversible.denied',
});

// ── appendComplianceAudit ─────────────────────────────────────────────────────

/**
 * Appends a compliance audit row to audit_logs. INSERT only — never UPDATE/DELETE.
 *
 * @param {object} entry
 * @param {string} entry.user_id           Required in supabase mode; 'local-user-1' in local mode
 * @param {string} entry.org_id            Required
 * @param {string} [entry.project_id]      Optional
 * @param {string} entry.action            Required — use ComplianceActions constants
 * @param {string} [entry.action_target_id]   Optional
 * @param {string} [entry.action_target_type] Optional — 'artifact' | 'project' | 'user' | 'org' | 'tool'
 * @param {'success'|'denied'|'error'} entry.status  Required
 * @param {string} entry.summary           Human-readable one-liner
 * @param {object} [entry.meta]            Optional JSON-serializable (no secrets, no content)
 * @param {string} [entry.ip]             Optional — from x-forwarded-for
 * @param {string} [entry.user_agent]      Optional — from user-agent header
 * @param {string} [entry.correlation_id]  Optional — propagated from parent request
 *
 * @returns {{ id: string }} The inserted row ID
 * @throws {Error} If required fields are missing
 */
export function appendComplianceAudit({
  user_id,
  org_id,
  project_id = null,
  action,
  action_target_id = null,
  action_target_type = null,
  status,
  summary,
  meta = null,
  ip = null,
  user_agent = null,
  correlation_id = null,
}) {
  if (!action) throw new Error('appendComplianceAudit: action is required');
  if (!org_id) throw new Error('appendComplianceAudit: org_id is required');
  if (!status) throw new Error('appendComplianceAudit: status is required');
  if (!summary) throw new Error('appendComplianceAudit: summary is required');

  const db = getDb();
  const id = newId('audit');
  const at = nowIso();

  // Structural guarantee: this module never executes UPDATE or DELETE.
  // The insert below is the only write path. If future contributors add an
  // UPDATE or DELETE call here, the audit_logs triggers will also catch it,
  // but the intent is that no such code path is ever written.
  db.prepare(
    `INSERT INTO audit_logs
       (id, at, type, summary, status, meta,
        user_id, org_id, project_id, ip, user_agent,
        action_target_id, action_target_type, correlation_id)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
  ).run(
    id,
    at,
    action,
    summary,
    status,
    meta != null ? JSON.stringify(meta) : null,
    user_id || null,
    org_id,
    project_id,
    ip,
    user_agent,
    action_target_id,
    action_target_type,
    correlation_id
  );

  return { id };
}

// ── Request helpers ───────────────────────────────────────────────────────────

/**
 * Extracts the client IP from a Next.js request object.
 * Checks x-forwarded-for first, then x-real-ip.
 */
export function requestIp(req) {
  if (!req) return null;
  const xff =
    req?.headers?.get?.('x-forwarded-for') ||
    req?.headers?.['x-forwarded-for'];
  if (xff) return String(xff).split(',')[0].trim();
  const xri =
    req?.headers?.get?.('x-real-ip') ||
    req?.headers?.['x-real-ip'];
  if (xri) return String(xri).trim();
  return null;
}

/**
 * Extracts the user-agent from a Next.js request object.
 */
export function requestUserAgent(req) {
  if (!req) return null;
  return (
    req?.headers?.get?.('user-agent') ||
    req?.headers?.['user-agent'] ||
    null
  );
}
