const SCHEMA_VERSION = 1;
const BOUNDARIES = new Set(['app', 'api', 'queue', 'model']);
const STATUSES = new Set(['pass', 'fail', 'degraded', 'unknown']);

// Closed privacy-safe vocabulary: a failure class can never carry free text,
// secrets, medical notes, control characters, or terminal escapes because only
// these dotted lowercase slugs may ever reach rendering.
const FAILURE_CLASSES = new Set(['worker.unavailable', 'vision.timeout', 'model.error']);
const FAILURE_CLASS_PATTERN = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){1,2}$/;

const FORBIDDEN_TOP_KEYS = new Set([
  'session', 'sessionid', 'sessionidtoken', 'sessiontoken', 'cookie', 'cookies',
  'authorization', 'authheader', 'apikey', 'api_key', 'password', 'credential',
  'credentials', 'environment', 'envdump', 'photos', 'photo', 'image', 'images',
  'base64', 'note', 'notetext', 'notes', 'user', 'username', 'email', 'identity',
]);

const FORBIDDEN_CHECK_KEYS = new Set([
  ...FORBIDDEN_TOP_KEYS,
  'imagehash', 'base64payload', 'payload', 'blob', 'rawoutput',
]);

const SECRET_VALUE_PATTERNS = [
  /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
  /\b(?:sk|sess|ghp|glpat)-[A-Za-z0-9_-]{8,}\b/,
  /\bbearer\s+[A-Za-z0-9._+/=-]{16,}\b/i,
  /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/,
];

const COUNTER_KEYS = new Set(['ok', 'fail', 'abstain', 'retry', 'timeout', 'rejected', 'fallback', 'depth']);
const COUNTER_CEILING = 1_000_000;

function looksSensitive(value) {
  const text = typeof value === 'string' ? value : JSON.stringify(value);
  if (!text || text.length > 256) return true;
  return SECRET_VALUE_PATTERNS.some(pattern => pattern.test(text));
}

function boundedCounters(raw) {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
  const counters = {};
  for (const key of Object.keys(raw).sort()) {
    if (!COUNTER_KEYS.has(key)) continue;
    const value = Number(raw[key]);
    if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value) || value > COUNTER_CEILING) return null;
    counters[key] = value;
  }
  return counters;
}

function boundedLatency(raw) {
  const value = Number(raw);
  if (!Number.isFinite(value) || value < 0) return null;
  if (value > 3_600_000) return null;
  return Math.round(value);
}

function buildCheck(raw) {
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return { dropped: true };
  const id = String(raw.id || '');
  if (!/^[a-z][a-z0-9.-]{0,63}$/.test(id)) return { dropped: true };
  const boundary = String(raw.boundary || '');
  if (!BOUNDARIES.has(boundary)) return { dropped: true };
  const status = String(raw.status || '');
  if (!STATUSES.has(status)) return { dropped: true };
  const failureClass = raw.failureClass == null ? null : String(raw.failureClass);
  if (failureClass !== null && (!FAILURE_CLASSES.has(failureClass) || !FAILURE_CLASS_PATTERN.test(failureClass))) {
    return { rejected: true };
  }
  const counters = boundedCounters(raw.counters);
  if (counters === null) return { rejected: true };
  return {
    check: {
      id,
      boundary,
      status,
      failureClass,
      latencyMs: boundedLatency(raw.latencyMs),
      counters,
    },
    dropped: Object.keys(raw).some(key => {
      if (['id', 'boundary', 'status', 'failureClass', 'latencyMs', 'counters'].includes(key)) return false;
      if (FORBIDDEN_CHECK_KEYS.has(key.replace(/[^a-z0-9]/gi, '').toLowerCase())) return true;
      return looksSensitive(raw[key]);
    }),
  };
}

export function sanitizeEvidence(input) {
  if (!input || typeof input !== 'object' || Array.isArray(input)) return { ok: false, evidence: null };
  const commit = String(input.commit || '');
  if (!/^[0-9a-f]{12,40}$/.test(commit)) return { ok: false, evidence: null };
  const releaseTag = /^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(String(input.releaseTag || '')) ? input.releaseTag : null;
  if (!releaseTag) return { ok: false, evidence: null };
  const generatedAtUtc = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(String(input.generatedAtUtc || ''))
    ? input.generatedAtUtc
    : null;
  if (!generatedAtUtc) return { ok: false, evidence: null };
  if (!Array.isArray(input.checks) || input.checks.length === 0 || input.checks.length > 100) {
    return { ok: false, evidence: null };
  }

  const checks = [];
  for (const raw of input.checks) {
    const built = buildCheck(raw);
    if (built.rejected) return { ok: false, evidence: null };
    if (built.dropped) continue;
    checks.push(built.check);
  }
  if (checks.length === 0) return { ok: false, evidence: null };
  // Fail closed on partial telemetry: an evidence set that is missing any of
  // the four boundaries cannot be distinguished from a censored one, so the
  // dashboard refuses it instead of rendering silent gaps as healthy.
  const presentBoundaries = new Set(checks.map(check => check.boundary));
  for (const boundary of BOUNDARIES) {
    if (!presentBoundaries.has(boundary)) return { ok: false, evidence: null };
  }
  if (Number(input.schemaVersion) !== SCHEMA_VERSION) return { ok: false, evidence: null };

  return {
    ok: true,
    evidence: {
      schemaVersion: SCHEMA_VERSION,
      releaseTag,
      commit,
      generatedAtUtc,
      checks,
    },
  };
}

function emptyBoundary() {
  return { checks: 0, pass: 0, degraded: 0, fail: 0, unknown: 0, latencyMs: null, counters: {} };
}

export function buildDashboard(evidence) {
  const validated = sanitizeEvidence(evidence);
  if (!validated.ok) return { ok: false, boundaries: null, totals: null, failureClasses: null, manualFallback: null };
  const clean = validated.evidence;

  const boundaries = { app: emptyBoundary(), api: emptyBoundary(), queue: emptyBoundary(), model: emptyBoundary() };
  const failureClasses = {};
  const totals = { checks: 0, pass: 0, degraded: 0, fail: 0, unknown: 0 };

  for (const check of clean.checks) {
    const bucket = boundaries[check.boundary];
    bucket.checks += 1;
    bucket[check.status] += 1;
    if (check.latencyMs != null && (bucket.latencyMs == null || check.latencyMs > bucket.latencyMs)) {
      bucket.latencyMs = check.latencyMs;
    }
    for (const key of Object.keys(check.counters)) {
      bucket.counters[key] = (bucket.counters[key] || 0) + check.counters[key];
    }
    if (check.failureClass) {
      failureClasses[check.failureClass] = (failureClasses[check.failureClass] || 0) + 1;
    }
    totals.checks += 1;
    totals[check.status] += 1;
  }

  // Deterministic manual-fallback contract: the local journal is a usable
  // fallback exactly while the app boundary itself can still serve it. Any
  // degradation anywhere makes the journal the active fallback ('local-journal');
  // only an app-boundary failure removes it ('app-down').
  const anyDegradation = totals.fail > 0 || totals.degraded > 0;
  const manualFallback = boundaries.app.fail > 0
    ? { available: false, reason: 'app-down' }
    : anyDegradation
      ? { available: true, reason: 'local-journal' }
      : { available: true, reason: 'none-required' };

  return {
    ok: true,
    identity: { releaseTag: clean.releaseTag, commit: clean.commit, generatedAtUtc: clean.generatedAtUtc },
    boundaries,
    totals,
    failureClasses,
    manualFallback,
  };
}

export const DEFAULT_ALERT_RULES = [
  {
    id: 'worker.outage',
    severity: 'page',
    owner: 'release-operator',
    threshold: 1,
    runbook: 'docs/RELEASE-OBSERVABILITY.md#simulate-a-worker-outage',
    count: dashboard =>
      Math.max(
        dashboard.failureClasses['worker.unavailable'] || 0,
        dashboard.boundaries.model.fail > 0 && dashboard.boundaries.queue.fail > 0
          ? dashboard.boundaries.model.fail + dashboard.boundaries.queue.fail + dashboard.boundaries.api.fail
          : 0,
      ),
  },
  {
    id: 'telemetry.gap',
    severity: 'warn',
    owner: 'release-operator',
    threshold: 1,
    runbook: 'docs/RELEASE-OBSERVABILITY.md#alert-inventory',
    count: dashboard => dashboard.totals.unknown,
  },
  {
    id: 'queue.backlog',
    severity: 'warn',
    owner: 'release-operator',
    threshold: 10,
    runbook: 'docs/RELEASE-OBSERVABILITY.md#alert-inventory',
    count: dashboard => dashboard.boundaries.queue.counters.depth || 0,
  },
  {
    id: 'vision.degraded',
    severity: 'warn',
    owner: 'vision-owner',
    threshold: 3,
    runbook: 'docs/RELEASE-OBSERVABILITY.md#alert-inventory',
    count: dashboard => dashboard.failureClasses['vision.timeout'] || 0,
  },
  {
    id: 'app.unhealthy',
    severity: 'page',
    owner: 'release-operator',
    threshold: 1,
    runbook: 'docs/RELEASE-OBSERVABILITY.md#alert-inventory',
    count: dashboard => dashboard.boundaries.app.fail,
  },
];

export function evaluateAlerts(dashboard, rules = DEFAULT_ALERT_RULES) {
  if (!dashboard || !dashboard.ok) return [];
  const alerts = [];
  for (const rule of rules) {
    let count = 0;
    try {
      count = Number(rule.count(dashboard));
    } catch {
      count = 0;
    }
    if (!Number.isFinite(count) || count <= 0 || count < rule.threshold) continue;
    // Correlation: while a worker outage is paging, queue depth is residual
    // noise from the same incident, so the backlog warn would only duplicate
    // the page. It stays active the moment the page clears.
    if (rule.id === 'queue.backlog' && alerts.some(alert => alert.id === 'worker.outage')) continue;
    alerts.push({
      id: rule.id,
      severity: rule.severity,
      owner: rule.owner,
      threshold: rule.threshold,
      count,
      runbook: rule.runbook,
      message:
        rule.id === 'worker.outage'
          ? `${rule.severity.toUpperCase()} worker.unavailable on ${dashboard.identity.releaseTag} (${dashboard.identity.commit.slice(0, 12)}): ${count} failing boundary checks — follow ${rule.runbook} and keep the local journal as fallback.`
          : `${rule.severity.toUpperCase()} ${rule.id} on ${dashboard.identity.releaseTag}: observed ${count} (threshold ${rule.threshold}) — see ${rule.runbook}.`,
    });
  }
  return alerts;
}
