/**
 * Telemetry REST endpoints.
 *
 * POST   /telemetry/events              — batch ingest (any auth)
 * GET    /telemetry/config              — collection config for clients (ETag)
 * GET    /telemetry/query               — admin query
 * GET    /telemetry/clusters            — admin error clusters
 * PATCH  /telemetry/clusters/:id        — resolve/ignore/reopen cluster (admin)
 * POST   /telemetry/policies/preview    — count matching clients for targeting (admin)
 * GET    /telemetry/policies            — list policies (admin)
 * POST   /telemetry/policies            — create policy (admin)
 * PUT    /telemetry/policies/:id        — update policy (admin)
 * DELETE /telemetry/policies/:id        — delete policy (admin)
 * DELETE /telemetry/user/:userId        — GDPR erasure (admin)
 * GET    /telemetry/geo                 — geo distribution by country (admin)
 * GET    /telemetry/metrics             — ingestion metrics JSON (admin)
 * GET    /telemetry/metrics/prometheus  — Prometheus OpenMetrics export (admin)
 */

import type { FastifyInstance } from 'fastify';
import { randomUUID } from 'node:crypto';
import { createHash } from 'node:crypto';
import { getRequestProductId } from '../../lib/request-context.js';
import { BadRequestError, NotFoundError, UnauthorizedError } from '../../lib/errors.js';
import { hashUserFlag } from '../flags/routes.js';
import * as repo from './repository.js';
import {
  TelemetryIngestRequestSchema,
  CreatePolicySchema,
  UpdatePolicySchema,
  UpdateClusterSchema,
  TelemetryQuerySchema,
  type TelemetryEventDoc,
  type TelemetryCollectionPolicyDoc,
  type TelemetryCollectionConfig,
  type TelemetryErrorCluster,
  type TelemetryMetrics,
} from './types.js';
import * as auditRepo from '../audit/repository.js';
import type { AuditDoc } from '../audit/types.js';

// ─── Helpers ────────────────────────────────────────────────────────

const DEFAULT_EVENT_TTL_DAYS = parseInt(process.env.TELEMETRY_EVENT_TTL_DAYS ?? '30', 10);
const DEFAULT_CLUSTER_TTL_DAYS = parseInt(process.env.TELEMETRY_CLUSTER_TTL_DAYS ?? '90', 10);
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED !== 'false';
const PII_SCAN_ENABLED = process.env.TELEMETRY_PII_SCAN_ENABLED !== 'false';
const CLIENT_BATCH_SIZE = parseInt(process.env.TELEMETRY_CLIENT_BATCH_SIZE ?? '20', 10);
const CLIENT_FLUSH_MS = parseInt(process.env.TELEMETRY_CLIENT_FLUSH_MS ?? '60000', 10);
const CLIENT_MAX_QUEUE = parseInt(process.env.TELEMETRY_CLIENT_MAX_QUEUE ?? '200', 10);
const RATE_LIMIT_PER_MIN = parseInt(process.env.TELEMETRY_RATE_LIMIT_PER_MIN ?? '100', 10);
const RATE_LIMIT_WINDOW_MS = 60_000;

// ─── Rate limiter (in-memory sliding window per installId) ──────

const rateBuckets = new Map<string, { count: number; windowStart: number }>();

export function checkRateLimit(installId: string, eventCount: number): boolean {
  const now = Date.now();
  const bucket = rateBuckets.get(installId);
  if (!bucket || now - bucket.windowStart > RATE_LIMIT_WINDOW_MS) {
    rateBuckets.set(installId, { count: eventCount, windowStart: now });
    return true;
  }
  if (bucket.count + eventCount > RATE_LIMIT_PER_MIN) {
    return false;
  }
  bucket.count += eventCount;
  return true;
}

// Periodic cleanup of stale rate-limit buckets (every 5 min)
const _cleanupTimer = globalThis.setInterval(() => {
  const cutoff = Date.now() - RATE_LIMIT_WINDOW_MS * 2;
  for (const [key, bucket] of rateBuckets) {
    if (bucket.windowStart < cutoff) rateBuckets.delete(key);
  }
}, 300_000);
if (typeof _cleanupTimer === 'object' && 'unref' in _cleanupTimer) _cleanupTimer.unref();

// ─── Ingestion metrics (in-memory counters) ─────────────────────

const metrics: TelemetryMetrics = {
  totalEventsIngested: 0,
  totalEventsRejected: 0,
  totalBatchRequests: 0,
  totalRateLimited: 0,
  totalPiiBlocked: 0,
  totalDuplicatesDropped: 0,
  uptimeSince: new Date().toISOString(),
};

// ─── Webhook alerting ───────────────────────────────────────────

const ALERT_WEBHOOK_URL = process.env.TELEMETRY_ALERT_WEBHOOK_URL ?? '';
const ALERT_SEVERITY_THRESHOLD = (process.env.TELEMETRY_ALERT_SEVERITY_THRESHOLD ?? 'error') as
  | 'warn'
  | 'error'
  | 'fatal';
const ALERT_COUNT_THRESHOLD = parseInt(process.env.TELEMETRY_ALERT_COUNT_THRESHOLD ?? '10', 10);

async function sendClusterAlert(
  cluster: TelemetryErrorCluster,
  previousSeverity: string
): Promise<void> {
  if (!ALERT_WEBHOOK_URL) return;

  const severityOrder: Record<string, number> = { warn: 0, error: 1, fatal: 2 };
  const thresholdNum = severityOrder[ALERT_SEVERITY_THRESHOLD] ?? 1;
  const clusterNum = severityOrder[cluster.severity] ?? 0;

  // Alert if severity meets threshold OR count exceeds threshold
  if (clusterNum < thresholdNum && cluster.totalCount < ALERT_COUNT_THRESHOLD) return;

  const payload = {
    text: `Telemetry Alert: Cluster ${cluster.fingerprint} escalated`,
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: [
            `*Telemetry Error Cluster Escalated*`,
            `*Event:* ${cluster.module}/${cluster.eventName}`,
            `*Platform:* ${cluster.platform} (${cluster.channel})`,
            `*Severity:* ${previousSeverity} → *${cluster.severity}*`,
            `*Total Count:* ${cluster.totalCount}`,
            `*Affected Users:* ${cluster.affectedUserIds.length + cluster.affectedInstallIds.length}`,
            `*First Seen:* ${cluster.firstSeenAt}`,
            `*Last Seen:* ${cluster.lastSeenAt}`,
            cluster.sampleMessage ? `*Sample:* \`${cluster.sampleMessage.slice(0, 200)}\`` : '',
          ]
            .filter(Boolean)
            .join('\n'),
        },
      },
    ],
  };

  try {
    await fetch(ALERT_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(payload),
    });
  } catch {
    // Best-effort — don't fail ingestion on alert failure
  }
}

// ─── Audit helper ───────────────────────────────────────────────

function emitAudit(
  productId: string,
  userId: string,
  action: string,
  details: Record<string, unknown>
): void {
  const doc: AuditDoc = {
    id: `aud_${randomUUID()}`,
    productId,
    userId,
    action,
    category: 'telemetry',
    details,
    createdAt: new Date().toISOString(),
  };
  auditRepo.create(doc).catch(() => {});
}

// ─── Geo enrichment (best-effort IP → country/region) ─────────

const GEO_ENABLED = process.env.TELEMETRY_GEO_ENABLED === 'true';
const GEO_API_URL = process.env.TELEMETRY_GEO_API_URL ?? ''; // e.g. http://ip-api.com/json/{ip}?fields=countryCode,region

interface GeoResult {
  countryCode?: string;
  regionCode?: string;
}

const geoCache = new Map<string, { result: GeoResult; ts: number }>();
const GEO_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 h

async function lookupGeo(ip: string): Promise<GeoResult> {
  if (!GEO_ENABLED || !GEO_API_URL || !ip || ip === '127.0.0.1' || ip === '::1') return {};

  const cached = geoCache.get(ip);
  if (cached && Date.now() - cached.ts < GEO_CACHE_TTL_MS) return cached.result;

  try {
    const url = GEO_API_URL.replace('{ip}', encodeURIComponent(ip));
    const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
    if (!res.ok) return {};
    const data = (await res.json()) as Record<string, string>;
    const result: GeoResult = {
      countryCode: data.countryCode || undefined,
      regionCode:
        data.countryCode && data.region ? `${data.countryCode}:${data.region}` : undefined,
    };
    geoCache.set(ip, { result, ts: Date.now() });
    // Cap cache size
    if (geoCache.size > 10_000) {
      const oldest = geoCache.keys().next().value;
      if (oldest) geoCache.delete(oldest);
    }
    return result;
  } catch {
    return {};
  }
}

export function extractClientIp(req: {
  headers: Record<string, string | string[] | undefined>;
  ip?: string;
}): string {
  const xff = req.headers['x-forwarded-for'];
  if (typeof xff === 'string') return xff.split(',')[0].trim();
  if (Array.isArray(xff) && xff.length > 0) return xff[0].split(',')[0].trim();
  return req.ip ?? '';
}

/** PII patterns — reject events containing these. */
const PII_PATTERNS = [
  /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/i, // email
  /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/, // US phone
  /\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b/, // credit card
  /\b\d{3}-\d{2}-\d{4}\b/, // SSN
];

export function containsPII(text: string): boolean {
  if (!PII_SCAN_ENABLED) return false;
  return PII_PATTERNS.some(p => p.test(text));
}

export function computePk(productId: string, occurredAt: string, platform: string): string {
  const date = new Date(occurredAt);
  const yyyyMM = `${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
  return `${productId}:${yyyyMM}:${platform}`;
}

export function normalizeMessage(msg: string): string {
  return msg
    .replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<UUID>')
    .replace(/\d+/g, '<N>')
    .replace(/\/[\w/.]+/g, '<PATH>')
    .toLowerCase()
    .trim();
}

export function generateFingerprint(event: TelemetryEventDoc): string {
  const input = [
    event.platform,
    event.channel,
    event.module,
    event.eventName,
    event.errorDomain ?? '',
    event.errorCode ?? '',
    normalizeMessage(event.message ?? ''),
  ].join(':');
  return createHash('sha256').update(input).digest('hex').substring(0, 16);
}

function requireAdmin(req: { jwtPayload?: { role?: string } }): void {
  if (req.jwtPayload?.role !== 'admin') {
    throw new UnauthorizedError('Admin access required');
  }
}

// ─── Policy evaluation helpers ──────────────────────────────────────

interface ClientContext {
  userId?: string;
  anonymousInstallId?: string;
  platform?: string;
  channel?: string;
  osFamily?: string;
  appVersion?: string;
  buildNumber?: string;
  releaseChannel?: string;
  countryCode?: string;
  regionCode?: string;
}

export function policyMatchesContext(
  policy: TelemetryCollectionPolicyDoc,
  ctx: ClientContext
): boolean {
  const t = policy.targeting;

  // Check time bounds
  const now = new Date().toISOString();
  if (policy.startsAt && now < policy.startsAt) return false;
  if (policy.expiresAt && now > policy.expiresAt) return false;

  // User targeting
  if (t.userIds?.length && ctx.userId && !t.userIds.includes(ctx.userId)) return false;
  if (
    t.anonymousInstallIds?.length &&
    ctx.anonymousInstallId &&
    !t.anonymousInstallIds.includes(ctx.anonymousInstallId)
  )
    return false;

  // Platform targeting
  if (t.platforms?.length && ctx.platform && !t.platforms.includes(ctx.platform as never))
    return false;
  if (t.channels?.length && ctx.channel && !t.channels.includes(ctx.channel as never)) return false;
  if (t.osFamilies?.length && ctx.osFamily && !t.osFamilies.includes(ctx.osFamily as never))
    return false;

  // Version targeting
  if (t.appVersions?.length && ctx.appVersion && !t.appVersions.includes(ctx.appVersion))
    return false;
  if (t.buildNumbers?.length && ctx.buildNumber && !t.buildNumbers.includes(ctx.buildNumber))
    return false;
  if (t.buildNumberRange && ctx.buildNumber) {
    const bn = parseInt(ctx.buildNumber, 10);
    if (!isNaN(bn)) {
      if (t.buildNumberRange.min !== undefined && bn < t.buildNumberRange.min) return false;
      if (t.buildNumberRange.max !== undefined && bn > t.buildNumberRange.max) return false;
    }
  }

  // Region targeting
  if (t.countryCodes?.length && ctx.countryCode && !t.countryCodes.includes(ctx.countryCode))
    return false;
  if (t.regionCodes?.length && ctx.regionCode && !t.regionCodes.includes(ctx.regionCode))
    return false;

  // Release channel targeting
  if (
    t.releaseChannels?.length &&
    ctx.releaseChannel &&
    !t.releaseChannels.includes(ctx.releaseChannel as never)
  )
    return false;

  // Percentage rollout (deterministic)
  if (t.percentage !== undefined && t.percentage < 100) {
    const identity = ctx.userId || ctx.anonymousInstallId || '';
    if (!identity) return false;
    const bucket = hashUserFlag(identity, `telemetry_policy_${policy.id}`);
    if (bucket >= t.percentage) return false;
  }

  return true;
}

export function mergePolicies(policies: TelemetryCollectionPolicyDoc[]): TelemetryCollectionConfig {
  if (policies.length === 0) {
    // Hardcoded default
    return {
      enabled: TELEMETRY_ENABLED,
      eventTypes: ['warn', 'error', 'fatal'],
      modules: [],
      samplingRates: { debug: 0, info: 0, warn: 0.5, error: 1, fatal: 1 },
      batchSize: CLIENT_BATCH_SIZE,
      flushIntervalMs: CLIENT_FLUSH_MS,
      maxQueueSize: CLIENT_MAX_QUEUE,
    };
  }

  // Policies are already sorted by priority DESC
  const eventTypesSet = new Set<string>();
  const modulesSet = new Set<string>();
  const rates: Record<string, number> = { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 };
  let enabled = TELEMETRY_ENABLED;

  for (const p of policies) {
    // Union event types
    for (const et of p.eventTypes) {
      eventTypesSet.add(et);
      // Highest-priority policy's rate wins per event type
      if (rates[et] === 0) {
        rates[et] = p.samplingRate;
      }
    }
    // Union modules
    for (const m of p.modules) {
      modulesSet.add(m);
    }
    // samplingRate 0 from highest-priority policy disables everything
    if (p.priority >= 999 && p.samplingRate === 0) {
      enabled = false;
    }
  }

  return {
    enabled,
    eventTypes: Array.from(eventTypesSet),
    modules: Array.from(modulesSet),
    samplingRates: {
      debug: rates.debug,
      info: rates.info,
      warn: rates.warn,
      error: rates.error,
      fatal: rates.fatal,
    },
    batchSize: CLIENT_BATCH_SIZE,
    flushIntervalMs: CLIENT_FLUSH_MS,
    maxQueueSize: CLIENT_MAX_QUEUE,
  };
}

// ─── Cluster update ─────────────────────────────────────────────────

async function updateClusterForEvent(event: TelemetryEventDoc): Promise<void> {
  if (!['warn', 'error', 'fatal'].includes(event.eventType)) return;

  const fingerprint = event.fingerprint || generateFingerprint(event);
  const date = new Date(event.occurredAt);
  const yyyyMM = `${date.getUTCFullYear()}${String(date.getUTCMonth() + 1).padStart(2, '0')}`;
  const clusterId = `${fingerprint}:${yyyyMM}`;
  const pk = `${event.productId}:${event.platform}:${event.module}`;

  const existing = await repo.getCluster(clusterId, pk);

  if (existing) {
    existing.totalCount++;
    existing.lastSeenAt = event.receivedAt;
    existing.sampleMessage = event.message;
    existing.sampleErrorDomain = event.errorDomain;
    existing.sampleErrorCode = event.errorCode;

    // Update affected users (dedup, cap 100)
    const uid = event.userId || event.anonymousInstallId;
    if (uid && !existing.affectedUserIds.includes(uid)) {
      if (existing.affectedUserIds.length < 100) existing.affectedUserIds.push(uid);
    }
    if (
      event.anonymousInstallId &&
      !existing.affectedInstallIds.includes(event.anonymousInstallId)
    ) {
      if (existing.affectedInstallIds.length < 100)
        existing.affectedInstallIds.push(event.anonymousInstallId);
    }
    if (!existing.affectedOsFamilies.includes(event.osFamily)) {
      existing.affectedOsFamilies.push(event.osFamily);
    }

    // Update version breakdown
    const vEntry = existing.affectedVersions.find(
      v => v.appVersion === event.appVersion && v.buildNumber === event.buildNumber
    );
    if (vEntry) {
      vEntry.count++;
      vEntry.lastSeenAt = event.receivedAt;
    } else if (existing.affectedVersions.length < 50) {
      existing.affectedVersions.push({
        appVersion: event.appVersion,
        buildNumber: event.buildNumber,
        count: 1,
        lastSeenAt: event.receivedAt,
      });
    }

    // Escalate severity + alert on escalation
    const severityOrder = { warn: 0, error: 1, fatal: 2 };
    const eventSev = event.eventType as 'warn' | 'error' | 'fatal';
    const previousSeverity = existing.severity;
    if ((severityOrder[eventSev] ?? 0) > (severityOrder[existing.severity] ?? 0)) {
      existing.severity = eventSev;
    }

    // Default status for legacy clusters without status field
    if (!existing.status) existing.status = 'open';

    await repo.upsertCluster(existing);

    // Fire webhook alert on severity escalation
    if (existing.severity !== previousSeverity) {
      sendClusterAlert(existing, previousSeverity).catch(() => {});
    }
  } else {
    const newCluster: TelemetryErrorCluster = {
      id: clusterId,
      pk,
      productId: event.productId,
      fingerprint,
      platform: event.platform,
      channel: event.channel,
      module: event.module,
      eventName: event.eventName,
      affectedVersions: [
        {
          appVersion: event.appVersion,
          buildNumber: event.buildNumber,
          count: 1,
          lastSeenAt: event.receivedAt,
        },
      ],
      firstSeenAt: event.receivedAt,
      lastSeenAt: event.receivedAt,
      totalCount: 1,
      affectedUserIds: event.userId ? [event.userId] : [],
      affectedInstallIds: event.anonymousInstallId ? [event.anonymousInstallId] : [],
      affectedOsFamilies: [event.osFamily],
      sampleErrorDomain: event.errorDomain,
      sampleErrorCode: event.errorCode,
      sampleMessage: event.message,
      severity: event.eventType as 'warn' | 'error' | 'fatal',
      status: 'open',
      ttl: DEFAULT_CLUSTER_TTL_DAYS * 86400,
    };
    await repo.upsertCluster(newCluster);
  }
}

// ─── Routes ─────────────────────────────────────────────────────────

export async function telemetryRoutes(app: FastifyInstance) {
  // ── Batch ingest ──────────────────────────────────────────────
  app.post('/telemetry/events', async (req, reply) => {
    if (!TELEMETRY_ENABLED) {
      return { accepted: 0, rejected: 0, serverTime: new Date().toISOString() };
    }

    // Auth: JWT or X-Install-Token
    const installToken = req.headers['x-install-token'] as string | undefined;
    if (!req.jwtPayload && !installToken) {
      throw new UnauthorizedError('JWT or X-Install-Token required');
    }

    const parsed = TelemetryIngestRequestSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const { productId, events } = parsed.data;

    // Rate limiting per installId
    const rateLimitKey = installToken || req.jwtPayload?.sub || 'unknown';
    if (!checkRateLimit(rateLimitKey, events.length)) {
      metrics.totalRateLimited++;
      reply.code(429);
      return {
        accepted: 0,
        rejected: events.length,
        errors: [{ index: 0, reason: `Rate limit exceeded (${RATE_LIMIT_PER_MIN} events/min)` }],
        serverTime: new Date().toISOString(),
      };
    }

    // Batch deduplication by event.id
    const seenIds = new Set<string>();
    const dedupedEvents = events.filter(e => {
      if (seenIds.has(e.id)) return false;
      seenIds.add(e.id);
      return true;
    });
    const dupCount = events.length - dedupedEvents.length;
    metrics.totalDuplicatesDropped += dupCount;
    metrics.totalBatchRequests++;

    // Geo enrichment (best-effort, non-blocking)
    const clientIp = extractClientIp(
      req as unknown as { headers: Record<string, string | string[] | undefined>; ip?: string }
    );
    const geo = await lookupGeo(clientIp);

    const now = new Date().toISOString();
    const ttl = DEFAULT_EVENT_TTL_DAYS * 86400;

    let accepted = 0;
    let rejected = dupCount; // duplicates
    const errors: Array<{ index: number; reason: string }> = [];
    const docsToInsert: TelemetryEventDoc[] = [];

    for (let i = 0; i < dedupedEvents.length; i++) {
      const event = dedupedEvents[i];

      // Validate productId matches request-level
      if (event.productId !== productId) {
        errors.push({ index: i, reason: `productId mismatch: expected ${productId}` });
        rejected++;
        continue;
      }

      // PII scan
      const fieldsToScan = [event.message, event.eventName, event.errorDomain].filter(Boolean);
      if (fieldsToScan.some(f => containsPII(f!))) {
        errors.push({ index: i, reason: 'PII detected' });
        rejected++;
        metrics.totalPiiBlocked++;
        continue;
      }

      const pk = computePk(productId, event.occurredAt, event.platform);
      const doc: TelemetryEventDoc = {
        ...event,
        pk,
        receivedAt: now,
        ttl,
        // Geo enrichment from server-side IP lookup
        countryCode: geo.countryCode,
        regionCode: geo.regionCode,
      };

      docsToInsert.push(doc);
      accepted++;
    }

    // Batch upsert (idempotent by event id)
    if (docsToInsert.length > 0) {
      await repo.upsertEventsBatch(docsToInsert);

      // Fire-and-forget cluster updates for warn/error/fatal
      for (const doc of docsToInsert) {
        if (['warn', 'error', 'fatal'].includes(doc.eventType)) {
          updateClusterForEvent(doc).catch(err => {
            req.log.warn({ err, eventId: doc.id }, 'Cluster update failed');
          });
        }
      }
    }

    metrics.totalEventsIngested += accepted;
    metrics.totalEventsRejected += rejected;

    reply.code(accepted > 0 ? 200 : 400);
    return {
      accepted,
      rejected,
      errors: errors.length > 0 ? errors : undefined,
      serverTime: now,
    };
  });

  // ── Collection config (client poll) with ETag caching ─────────
  app.get('/telemetry/config', async (req, reply) => {
    if (!TELEMETRY_ENABLED) {
      return {
        enabled: false,
        eventTypes: [],
        modules: [],
        samplingRates: { debug: 0, info: 0, warn: 0, error: 0, fatal: 0 },
        batchSize: CLIENT_BATCH_SIZE,
        flushIntervalMs: CLIENT_FLUSH_MS,
        maxQueueSize: CLIENT_MAX_QUEUE,
      } satisfies TelemetryCollectionConfig;
    }

    const productId = getRequestProductId(req);
    const ctx: ClientContext = req.query as ClientContext;

    const allPolicies = await repo.listPolicies(productId);
    const activePolicies = allPolicies.filter(p => p.enabled && policyMatchesContext(p, ctx));

    const config = mergePolicies(activePolicies);

    // ETag caching: hash the config to avoid redundant client processing
    const etag = createHash('sha256').update(JSON.stringify(config)).digest('hex').substring(0, 16);
    const clientEtag = req.headers['if-none-match'];
    if (clientEtag === `"${etag}"`) {
      reply.code(304);
      return;
    }

    reply.header('ETag', `"${etag}"`);
    reply.header('Cache-Control', 'private, max-age=60');
    return config;
  });

  // ── Admin: query events ───────────────────────────────────────
  app.get('/telemetry/query', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const parsed = TelemetryQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const result = await repo.queryEvents(productId, parsed.data);
    return {
      events: result.events,
      total: result.events.length,
      continuationToken: result.continuationToken,
    };
  });

  // ── Admin: error clusters ─────────────────────────────────────
  app.get('/telemetry/clusters', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const {
      platform,
      module: mod,
      from,
      to,
      limit,
    } = req.query as {
      platform?: string;
      module?: string;
      from?: string;
      to?: string;
      limit?: string;
    };
    const clusters = await repo.listClusters(productId, {
      platform,
      module: mod,
      from,
      to,
      limit: limit ? parseInt(limit, 10) : undefined,
    });
    return { clusters, total: clusters.length };
  });

  // ── Admin: policy preview (count matching clients) ──────────
  app.post('/telemetry/policies/preview', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const targeting = (req.body as Record<string, unknown>)?.targeting as
      | Record<string, unknown>
      | undefined;

    // Fetch recent events (last 7 days, up to 500)
    const sevenDaysAgo = new Date(Date.now() - 7 * 86400000).toISOString();
    const { events } = await repo.queryEvents(productId, {
      from: sevenDaysAgo,
      limit: 500,
    });

    // Filter events that match targeting
    const matchedIds = new Set<string>();
    for (const evt of events) {
      if (
        targeting?.platforms &&
        Array.isArray(targeting.platforms) &&
        targeting.platforms.length > 0
      ) {
        if (!targeting.platforms.includes(evt.platform)) continue;
      }
      if (
        targeting?.channels &&
        Array.isArray(targeting.channels) &&
        targeting.channels.length > 0
      ) {
        if (!targeting.channels.includes(evt.channel)) continue;
      }
      if (
        targeting?.osFamilies &&
        Array.isArray(targeting.osFamilies) &&
        targeting.osFamilies.length > 0
      ) {
        if (!targeting.osFamilies.includes(evt.osFamily)) continue;
      }
      if (
        targeting?.releaseChannels &&
        Array.isArray(targeting.releaseChannels) &&
        targeting.releaseChannels.length > 0
      ) {
        if (!targeting.releaseChannels.includes(evt.releaseChannel)) continue;
      }
      matchedIds.add(evt.anonymousInstallId || evt.userId || evt.id);
    }

    return {
      matchedClients: matchedIds.size,
      totalClients: new Set(events.map(e => e.anonymousInstallId || e.userId || e.id)).size,
      sampleSize: events.length,
    };
  });

  // ── Admin: list policies ──────────────────────────────────────
  app.get('/telemetry/policies', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    return { policies: await repo.listPolicies(productId) };
  });

  // ── Admin: create policy ──────────────────────────────────────
  app.post('/telemetry/policies', async (req, reply) => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const parsed = CreatePolicySchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const now = new Date().toISOString();
    const doc: TelemetryCollectionPolicyDoc = {
      id: randomUUID(),
      productId,
      ...parsed.data,
      createdAt: now,
      updatedAt: now,
      createdBy: req.jwtPayload?.sub ?? 'unknown',
    };

    const created = await repo.createPolicy(doc);
    emitAudit(productId, doc.createdBy, 'telemetry.policy.created', {
      policyId: doc.id,
      name: doc.name,
    });
    reply.code(201);
    return created;
  });

  // ── Admin: update policy ──────────────────────────────────────
  app.put('/telemetry/policies/:id', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);

    const parsed = UpdatePolicySchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    // Convert null to undefined for optional fields (Zod nullable → TS optional)
    const updates: Record<string, unknown> = { ...parsed.data };
    if (updates.startsAt === null) updates.startsAt = undefined;
    if (updates.expiresAt === null) updates.expiresAt = undefined;

    const updated = await repo.updatePolicy(
      id,
      productId,
      updates as Partial<TelemetryCollectionPolicyDoc>
    );
    if (!updated) throw new NotFoundError('Policy not found');
    emitAudit(productId, req.jwtPayload?.sub ?? 'unknown', 'telemetry.policy.updated', {
      policyId: id,
      updates: parsed.data,
    });
    return updated;
  });

  // ── Admin: delete policy ──────────────────────────────────────
  app.delete('/telemetry/policies/:id', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    const productId = getRequestProductId(req);
    const deleted = await repo.deletePolicy(id, productId);
    if (!deleted) throw new NotFoundError('Policy not found');
    emitAudit(productId, req.jwtPayload?.sub ?? 'unknown', 'telemetry.policy.deleted', {
      policyId: id,
    });
    return { success: true };
  });

  // ── Admin: GDPR erasure ───────────────────────────────────────
  app.delete('/telemetry/user/:userId', async req => {
    requireAdmin(req);
    const { userId } = req.params as { userId: string };
    const productId = getRequestProductId(req);
    const eventsDeleted = await repo.deleteEventsByUserId(productId, userId);
    emitAudit(productId, req.jwtPayload?.sub ?? 'unknown', 'telemetry.gdpr.erasure', {
      targetUserId: userId,
      eventsDeleted,
    });
    return { userId, eventsDeleted, clustersUpdated: 0 };
  });

  // ── Admin: resolve/ignore cluster ───────────────────────────
  app.patch('/telemetry/clusters/:id', async req => {
    requireAdmin(req);
    const { id } = req.params as { id: string };
    const { pk } = req.query as { pk: string };
    if (!pk) throw new BadRequestError('pk query parameter required');

    const parsed = UpdateClusterSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const updates: Partial<TelemetryErrorCluster> = {
      status: parsed.data.status,
    };
    if (parsed.data.status === 'resolved' || parsed.data.status === 'ignored') {
      updates.resolvedBy = req.jwtPayload?.sub ?? 'unknown';
      updates.resolvedAt = new Date().toISOString();
    } else if (parsed.data.status === 'open') {
      // Clear resolution metadata when reopening
      updates.resolvedBy = undefined;
      updates.resolvedAt = undefined;
    }

    const updated = await repo.updateCluster(id, pk, updates);
    if (!updated) throw new NotFoundError('Cluster not found');

    const productId = getRequestProductId(req);
    emitAudit(
      productId,
      req.jwtPayload?.sub ?? 'unknown',
      `telemetry.cluster.${parsed.data.status}`,
      {
        clusterId: id,
        pk,
      }
    );

    return updated;
  });

  // ── Admin: geo distribution ─────────────────────────────
  app.get('/telemetry/geo', async req => {
    requireAdmin(req);
    const productId = getRequestProductId(req);
    const { from, to } = req.query as { from?: string; to?: string };
    const distribution = await repo.queryGeoDistribution(productId, from, to);
    return { distribution };
  });

  // ── Admin: ingestion metrics (JSON) ─────────────────────
  app.get('/telemetry/metrics', async req => {
    requireAdmin(req);
    return metrics;
  });

  // ── Admin: cross-product telemetry ──────────────────────
  app.get('/telemetry/cross-product', async req => {
    requireAdmin(req);
    const { products, from, to, limit } = req.query as {
      products?: string;
      from?: string;
      to?: string;
      limit?: string;
    };

    if (!products) {
      throw new BadRequestError('products query parameter required (comma-separated productIds)');
    }

    const productIds = products
      .split(',')
      .map(p => p.trim())
      .filter(Boolean);
    if (productIds.length === 0 || productIds.length > 20) {
      throw new BadRequestError('Provide 1-20 product IDs');
    }

    const parsedLimit = limit ? parseInt(limit, 10) : 20;
    const clampedLimit =
      Number.isFinite(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 100) : 20;

    const [summary, daily, clusters] = await Promise.all([
      repo.queryCrossProductSummary(productIds, from, to),
      repo.queryCrossProductDaily(productIds, from, to),
      repo.queryCrossProductClusters(productIds, clampedLimit),
    ]);

    return { summary, daily, clusters };
  });

  // ── Prometheus OpenMetrics export ──────────────────────
  app.get('/telemetry/metrics/prometheus', async (req, reply) => {
    requireAdmin(req);
    const lines = [
      '# HELP telemetry_events_ingested_total Total events successfully ingested',
      '# TYPE telemetry_events_ingested_total counter',
      `telemetry_events_ingested_total ${metrics.totalEventsIngested}`,
      '# HELP telemetry_events_rejected_total Total events rejected (PII, mismatch, etc.)',
      '# TYPE telemetry_events_rejected_total counter',
      `telemetry_events_rejected_total ${metrics.totalEventsRejected}`,
      '# HELP telemetry_batch_requests_total Total batch ingest requests',
      '# TYPE telemetry_batch_requests_total counter',
      `telemetry_batch_requests_total ${metrics.totalBatchRequests}`,
      '# HELP telemetry_rate_limited_total Total rate-limited requests',
      '# TYPE telemetry_rate_limited_total counter',
      `telemetry_rate_limited_total ${metrics.totalRateLimited}`,
      '# HELP telemetry_pii_blocked_total Total events blocked for PII',
      '# TYPE telemetry_pii_blocked_total counter',
      `telemetry_pii_blocked_total ${metrics.totalPiiBlocked}`,
      '# HELP telemetry_duplicates_dropped_total Total duplicate events dropped',
      '# TYPE telemetry_duplicates_dropped_total counter',
      `telemetry_duplicates_dropped_total ${metrics.totalDuplicatesDropped}`,
      '',
    ];
    reply.type('text/plain; version=0.0.4; charset=utf-8');
    return lines.join('\n');
  });
}
