/**
 * Feature flags REST endpoints — production-grade feature management.
 *
 * ── Flag CRUD ────────────────────────────────────────────────────────────
 * GET    /flags              — list all flags (admin)
 * GET    /flags/poll         — legacy boolean polling (backward-compat)
 * POST   /flags/evaluate     — full evaluation with context (multi-variate)
 * GET    /flags/:key         — get single flag
 * POST   /flags              — create flag
 * PUT    /flags/:key         — update flag
 * DELETE /flags/:key         — delete flag
 * POST   /flags/:key/toggle  — quick toggle enabled state
 * POST   /flags/:key/archive — archive/unarchive flag
 * POST   /flags/kill         — kill switch (disable all matching flags)
 *
 * ── Segments ─────────────────────────────────────────────────────────────
 * GET    /flags/segments           — list segments
 * GET    /flags/segments/:key      — get segment
 * POST   /flags/segments           — create segment
 * PUT    /flags/segments/:key      — update segment
 * DELETE /flags/segments/:key      — delete segment
 *
 * ── Audit ────────────────────────────────────────────────────────────────
 * GET    /flags/audit              — list audit entries
 * GET    /flags/audit/:flagKey     — audit for specific flag
 *
 * ── SSE ──────────────────────────────────────────────────────────────────
 * GET    /flags/stream             — real-time flag change stream
 */

import type { FastifyInstance, FastifyRequest } from 'fastify';
import { getRequestProductId } from '../../lib/request-context.js';
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
import { bus } from '../../lib/event-bus.js';
import * as repo from './repository.js';
import {
  CreateFlagSchema,
  UpdateFlagSchema,
  CreateSegmentSchema,
  UpdateSegmentSchema,
  EvaluateSchema,
  type FeatureFlagDoc,
  type FlagAuditDoc,
} from './types.js';
import { evaluateAllFlags, hashUserFlag, compareVersions } from './evaluator.js';

// Re-export for backward compatibility with existing tests
export { hashUserFlag, compareVersions };

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

function getActor(req: FastifyRequest): string {
  return req.jwtPayload?.sub ?? 'system';
}

function diffChanges(before: Partial<FeatureFlagDoc>, after: Partial<FeatureFlagDoc>): string[] {
  const changes: string[] = [];
  const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
  for (const k of keys) {
    const key = k as keyof FeatureFlagDoc;
    if (key === 'updatedAt' || key === 'updatedBy' || key === 'version') continue;
    const bVal = JSON.stringify(before[key]);
    const aVal = JSON.stringify(after[key]);
    if (bVal !== aVal) changes.push(key);
  }
  return changes;
}

async function recordAudit(
  productId: string,
  flagKey: string,
  action: FlagAuditDoc['action'],
  actor: string,
  before?: Partial<FeatureFlagDoc>,
  after?: Partial<FeatureFlagDoc>
): Promise<void> {
  const changes = before && after ? diffChanges(before, after) : undefined;
  const doc: FlagAuditDoc = {
    id: `faudit_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
    productId,
    flagKey,
    action,
    actor,
    before,
    after,
    changes,
    timestamp: new Date().toISOString(),
  };
  await repo.createAuditEntry(doc);
}

function buildDefaultVariations(flagType: string) {
  switch (flagType) {
    case 'boolean':
      return [
        { key: 'on', value: true, description: 'Flag is on' },
        { key: 'off', value: false, description: 'Flag is off' },
      ];
    case 'string':
      return [{ key: 'default', value: '', description: 'Default string value' }];
    case 'number':
      return [{ key: 'default', value: 0, description: 'Default number value' }];
    case 'json':
      return [{ key: 'default', value: {}, description: 'Default JSON value' }];
    default:
      return [
        { key: 'on', value: true },
        { key: 'off', value: false },
      ];
  }
}

// ── SSE connected clients ───────────────────────────────────────────────────

interface SseClient {
  productId: string;
  write: (data: string) => void;
  close: () => void;
}

const sseClients: Set<SseClient> = new Set();

function broadcastFlagChange(productId: string, flagKey: string, action: string): void {
  const event = JSON.stringify({
    type: 'flag_change',
    flagKey,
    action,
    timestamp: new Date().toISOString(),
  });
  for (const client of sseClients) {
    if (client.productId === productId) {
      try {
        client.write(`data: ${event}\n\n`);
      } catch {
        sseClients.delete(client);
      }
    }
  }
}

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

export async function flagRoutes(app: FastifyInstance) {
  // ── List all flags ──────────────────────────────────────────────────────
  app.get('/flags', async req => {
    const productId = getRequestProductId(req);
    const { archived, tag } = req.query as { archived?: string; tag?: string };
    let flags = await repo.list(productId);

    if (archived !== 'true') {
      flags = flags.filter(f => !f.archived);
    }
    if (tag) {
      flags = flags.filter(f => f.tags?.includes(tag));
    }

    return { flags, total: flags.length };
  });

  // ── Legacy boolean polling (backward-compat) ───────────────────────────
  app.get('/flags/poll', async req => {
    const productId = getRequestProductId(req);
    const query = req.query as {
      platform?: string;
      userId?: string;
      region?: string;
      osVersion?: string;
      appVersion?: string;
    };

    const allFlags = await repo.list(productId);
    const segments = await repo.listSegments(productId);

    const results = evaluateAllFlags(
      allFlags,
      {
        userId: query.userId,
        platform: query.platform,
        region: query.region,
        osVersion: query.osVersion,
        appVersion: query.appVersion,
      },
      segments
    );

    // Legacy format: { flags: { key: boolean }, productId }
    const flags: Record<string, boolean> = {};
    for (const [key, result] of Object.entries(results)) {
      flags[key] =
        result.reason !== 'off' &&
        result.reason !== 'prerequisite_failed' &&
        result.reason !== 'schedule_inactive' &&
        result.reason !== 'error';
    }
    return { flags, productId };
  });

  // ── Full evaluation (multi-variate) ────────────────────────────────────
  app.post('/flags/evaluate', async req => {
    const productId = getRequestProductId(req);
    const parsed = EvaluateSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }

    const allFlags = await repo.list(productId);
    const segments = await repo.listSegments(productId);
    const results = evaluateAllFlags(allFlags, parsed.data, segments);

    return { evaluations: results, productId };
  });

  // ── Get single flag ────────────────────────────────────────────────────
  app.get('/flags/:key', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const flag = await repo.getByKey(key, productId);
    if (!flag) throw new NotFoundError('Flag not found');
    return flag;
  });

  // ── Create flag ────────────────────────────────────────────────────────
  app.post('/flags', async (req, reply) => {
    const productId = getRequestProductId(req);
    const parsed = CreateFlagSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const input = parsed.data;
    const existing = await repo.getByKey(input.key, productId);
    if (existing) throw new BadRequestError(`Flag "${input.key}" already exists`);

    const variations = input.variations ?? buildDefaultVariations(input.flagType);
    const defaultVariation = input.defaultVariation ?? variations[0]?.key ?? 'on';
    const offVariation =
      input.offVariation ?? (input.flagType === 'boolean' ? 'off' : (variations[0]?.key ?? 'off'));

    const now = new Date().toISOString();
    const actor = getActor(req);
    const doc: FeatureFlagDoc = {
      id: `flag_${productId}_${input.key}`,
      productId,
      key: input.key,
      flagType: input.flagType,
      enabled: input.enabled,
      archived: false,
      description: input.description,
      tags: input.tags,
      variations,
      defaultVariation,
      offVariation,
      platforms: input.platforms,
      regions: input.regions,
      osVersions: input.osVersions,
      segments: input.segments,
      targetingRules: input.targetingRules,
      individualTargets: input.individualTargets,
      prerequisites: input.prerequisites,
      percentage: input.percentage,
      schedule: input.schedule,
      createdAt: now,
      updatedAt: now,
      createdBy: actor,
      updatedBy: actor,
      version: 1,
    };

    const created = await repo.create(doc);
    await recordAudit(productId, input.key, 'created', actor, undefined, created);
    broadcastFlagChange(productId, input.key, 'created');

    try {
      bus.emit('flag.created', { productId, flagKey: input.key, actor });
    } catch {
      /* best-effort */
    }

    reply.code(201);
    return created;
  });

  // ── Update flag ────────────────────────────────────────────────────────
  app.put('/flags/:key', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const flag = await repo.getByKey(key, productId);
    if (!flag) throw new NotFoundError('Flag not found');

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

    const actor = getActor(req);
    const before = JSON.parse(JSON.stringify(flag)) as FeatureFlagDoc;
    const updates = { ...parsed.data, updatedBy: actor, version: flag.version + 1 };
    const updated = await repo.update(flag.id, productId, updates);
    if (!updated) throw new NotFoundError('Flag update failed');

    await recordAudit(productId, key, 'updated', actor, before, updated);
    broadcastFlagChange(productId, key, 'updated');

    try {
      bus.emit('flag.updated', {
        productId,
        flagKey: key,
        actor,
        changes: diffChanges(before, updated),
      });
    } catch {
      /* best-effort */
    }

    return updated;
  });

  // ── Toggle flag ────────────────────────────────────────────────────────
  app.post('/flags/:key/toggle', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const flag = await repo.getByKey(key, productId);
    if (!flag) throw new NotFoundError('Flag not found');

    const actor = getActor(req);
    const before = JSON.parse(JSON.stringify(flag)) as FeatureFlagDoc;
    const updated = await repo.update(flag.id, productId, {
      enabled: !flag.enabled,
      updatedBy: actor,
      version: flag.version + 1,
    });
    if (!updated) throw new NotFoundError('Flag toggle failed');

    await recordAudit(productId, key, 'toggled', actor, before, updated);
    broadcastFlagChange(productId, key, 'toggled');

    try {
      bus.emit('flag.toggled', {
        productId,
        flagId: flag.id,
        flagKey: key,
        enabled: !flag.enabled,
        actor,
      });
    } catch {
      /* best-effort */
    }

    return updated;
  });

  // ── Archive/unarchive flag ─────────────────────────────────────────────
  app.post('/flags/:key/archive', async req => {
    const { key } = req.params as { key: string };
    const { archived } = (req.body as { archived?: boolean }) ?? {};
    const productId = getRequestProductId(req);
    const flag = await repo.getByKey(key, productId);
    if (!flag) throw new NotFoundError('Flag not found');

    const newArchived = archived ?? !flag.archived;
    const actor = getActor(req);
    const before = JSON.parse(JSON.stringify(flag)) as FeatureFlagDoc;
    const updated = await repo.update(flag.id, productId, {
      archived: newArchived,
      enabled: newArchived ? false : flag.enabled,
      updatedBy: actor,
      version: flag.version + 1,
    });
    if (!updated) throw new NotFoundError('Flag archive failed');

    await recordAudit(productId, key, 'archived', actor, before, updated);
    broadcastFlagChange(productId, key, newArchived ? 'archived' : 'unarchived');

    return updated;
  });

  // ── Delete flag ────────────────────────────────────────────────────────
  app.delete('/flags/:key', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const flag = await repo.getByKey(key, productId);
    if (!flag) throw new NotFoundError('Flag not found');

    const actor = getActor(req);
    await repo.remove(flag.id, productId);
    await recordAudit(productId, key, 'deleted', actor, flag, undefined);
    broadcastFlagChange(productId, key, 'deleted');

    try {
      bus.emit('flag.deleted', { productId, flagKey: key, actor });
    } catch {
      /* best-effort */
    }

    return { success: true };
  });

  // ── Kill switch ────────────────────────────────────────────────────────
  app.post('/flags/kill', async req => {
    const { platform, keys, tags } = req.body as {
      platform?: string;
      keys?: string[];
      tags?: string[];
    };
    const productId = getRequestProductId(req);
    const actor = getActor(req);
    const all = await repo.list(productId);
    const toDisable = all.filter(f => {
      if (!f.enabled) return false;
      if (keys && keys.length > 0 && !keys.includes(f.key)) return false;
      if (platform && f.platforms.length > 0 && !f.platforms.includes(platform)) return false;
      if (tags && tags.length > 0 && !tags.some(t => f.tags?.includes(t))) return false;
      return true;
    });

    const disabled: string[] = [];
    for (const f of toDisable) {
      const before = JSON.parse(JSON.stringify(f)) as FeatureFlagDoc;
      const afterDoc = await repo.update(f.id, productId, {
        enabled: false,
        updatedBy: actor,
        version: f.version + 1,
      });
      await recordAudit(
        productId,
        f.key,
        'kill_switch',
        actor,
        before,
        afterDoc ?? { ...f, enabled: false }
      );
      broadcastFlagChange(productId, f.key, 'kill_switch');
      disabled.push(f.key);
    }

    try {
      bus.emit('flag.kill_switch', { productId, disabled, actor });
    } catch {
      /* best-effort */
    }

    return { disabled, count: disabled.length };
  });

  // ── Segments CRUD ─────────────────────────────────────────────────────

  app.get('/flags/segments', async req => {
    const productId = getRequestProductId(req);
    const segments = await repo.listSegments(productId);
    return { segments, total: segments.length };
  });

  app.get('/flags/segments/:key', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const segment = await repo.getSegmentByKey(key, productId);
    if (!segment) throw new NotFoundError('Segment not found');
    return segment;
  });

  app.post('/flags/segments', async (req, reply) => {
    const productId = getRequestProductId(req);
    const parsed = CreateSegmentSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const input = parsed.data;
    const existing = await repo.getSegmentByKey(input.key, productId);
    if (existing) throw new BadRequestError(`Segment "${input.key}" already exists`);

    const now = new Date().toISOString();
    const doc = {
      id: `seg_${productId}_${input.key}`,
      productId,
      ...input,
      createdAt: now,
      updatedAt: now,
    };
    const created = await repo.createSegment(doc);
    reply.code(201);
    return created;
  });

  app.put('/flags/segments/:key', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const segment = await repo.getSegmentByKey(key, productId);
    if (!segment) throw new NotFoundError('Segment not found');

    const parsed = UpdateSegmentSchema.safeParse(req.body);
    if (!parsed.success) {
      throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
    }
    const updated = await repo.updateSegment(segment.id, productId, parsed.data);
    if (!updated) throw new NotFoundError('Segment update failed');
    return updated;
  });

  app.delete('/flags/segments/:key', async req => {
    const { key } = req.params as { key: string };
    const productId = getRequestProductId(req);
    const segment = await repo.getSegmentByKey(key, productId);
    if (!segment) throw new NotFoundError('Segment not found');
    await repo.removeSegment(segment.id, productId);
    return { success: true };
  });

  // ── Audit log ─────────────────────────────────────────────────────────

  app.get('/flags/audit', async req => {
    const productId = getRequestProductId(req);
    const { limit } = req.query as { limit?: string };
    const entries = await repo.listAuditLog(productId, undefined, limit ? parseInt(limit, 10) : 50);
    return { entries, total: entries.length };
  });

  app.get('/flags/audit/:flagKey', async req => {
    const { flagKey } = req.params as { flagKey: string };
    const productId = getRequestProductId(req);
    const { limit } = req.query as { limit?: string };
    const entries = await repo.listAuditLog(productId, flagKey, limit ? parseInt(limit, 10) : 50);
    return { entries, total: entries.length };
  });

  // ── SSE stream ────────────────────────────────────────────────────────

  app.get('/flags/stream', async (req, reply) => {
    // EventSource cannot set custom headers, so accept productId + token from query string
    const qs = req.query as { productId?: string; token?: string };
    if (qs.productId && !req.headers['x-product-id']) {
      (req.headers as Record<string, string>)['x-product-id'] = qs.productId;
    }
    if (qs.token && !req.headers['authorization']) {
      (req.headers as Record<string, string>)['authorization'] = `Bearer ${qs.token}`;
    }
    const productId = getRequestProductId(req);

    reply.raw.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
      'X-Accel-Buffering': 'no',
    });
    reply.hijack();

    const client: SseClient = {
      productId,
      write: (data: string) => reply.raw.write(data),
      close: () => reply.raw.end(),
    };
    sseClients.add(client);

    // Send initial connected event
    reply.raw.write(`data: ${JSON.stringify({ type: 'connected', productId })}\n\n`);

    // Heartbeat every 30s
    const heartbeat = setInterval(() => {
      try {
        reply.raw.write(': heartbeat\n\n');
      } catch {
        clearInterval(heartbeat);
        sseClients.delete(client);
      }
    }, 30_000);

    req.raw.on('close', () => {
      clearInterval(heartbeat);
      sseClients.delete(client);
    });
  });
}
