import type { FastifyInstance } from 'fastify';
import type { FastifySchema } from 'fastify';
import type pg from 'pg';
import { z } from 'zod';
import type { Authenticate } from '../../../shared/auth.js';
import { requireRole } from '../../../shared/auth.js';
import { AppError } from '../../../shared/errors.js';
import { errorSchema } from '../../../shared/swagger.js';
import { parseJson } from '../../../shared/http-input.js';
import { AuditLogger } from '../application/audit-logger.js';
import { RateLimiter } from '../application/rate-limiter.js';
import type { LogBroadcaster } from '../../../infrastructure/logging/log-broadcaster.js';

export interface SecurityRoutesDeps {
  pool: pg.Pool;
  authenticate: Authenticate;
  rateLimiter: RateLimiter;
  auditLogger: AuditLogger;
  broadcaster?: LogBroadcaster;
}

// ── Schema definitions ─────────────────────────────────────────────────────────
const userQuerySchema = z.object({
  limit: z.coerce.number().int().positive().max(100).optional().default(20),
  offset: z.coerce.number().int().min(0).optional().default(0),
  role: z.enum(['customer', 'editor', 'admin']).optional(),
  q: z.string().optional(),
});
const createUserBodySchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  role: z.enum(['customer', 'editor', 'admin']).default('customer'),
});
const updateUserBodySchema = z.object({
  role: z.enum(['customer', 'editor', 'admin']).optional(),
  password: z.string().min(8).optional(),
});

// ── Helpers ────────────────────────────────────────────────────────────────────
function isPgUniqueViolation(err: unknown): boolean {
  return typeof err === 'object' && err !== null && (err as { code?: string }).code === '23505';
}

export async function registerSecurityRoutes(
  app: FastifyInstance,
  deps: SecurityRoutesDeps,
): Promise<void> {
  const mfaStatusSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'MFA status',
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/admin/mfa/status', { schema: mfaStatusSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const result = await deps.pool.query<{ mfa_enrolled: boolean }>(
      'SELECT mfa_enrolled FROM identity_users WHERE id = $1',
      [user.id],
    );
    const row = result.rows[0];
    return reply.send({ mfaEnrolled: row?.mfa_enrolled ?? false });
  });

  const enrollMfaSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Enroll MFA',
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/admin/mfa/enroll', { schema: enrollMfaSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    await deps.pool.query('UPDATE identity_users SET mfa_enrolled = true WHERE id = $1', [user.id]);
    await deps.auditLogger.log({
      actorId: user.id,
      action: 'admin.mfa.enroll',
      target: user.id,
      metadata: {},
    });
    return reply.send({ mfaEnrolled: true });
  });

  const auditLogSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Write audit log',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/admin/audit/log', { schema: auditLogSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const input = parseJson(
      z.object({
        action: z.string().min(1).max(120),
        target: z.string().min(1).max(200),
        metadata: z.record(z.string(), z.unknown()).optional(),
      }),
      request.body,
    );
    await deps.auditLogger.log({
      actorId: user.id,
      action: input.action,
      target: input.target,
      metadata: input.metadata ?? {},
    });
    return reply.code(201).send({ ok: true });
  });

  const getAuditSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Get audit log',
    querystring: {
      type: 'object',
      properties: {
        actorId: { type: 'string' },
        action: { type: 'string' },
        limit: { type: 'integer', default: 50 },
        offset: { type: 'integer', default: 0 },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/admin/audit', { schema: getAuditSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const { actorId, action, limit, offset } = parseJson(
      z.object({
        actorId: z.string().optional(),
        action: z.string().optional(),
        limit: z.coerce.number().int().positive().max(200).optional().default(50),
        offset: z.coerce.number().int().min(0).optional().default(0),
      }),
      request.query ?? {},
    );
    const result = await deps.auditLogger.list({ actorId, action, limit, offset });
    return reply.send(result);
  });

  const rateLimitCheckSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Check rate limit',
    querystring: { type: 'object', required: ['key'], properties: { key: { type: 'string' } } },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/admin/rate-limit/check', { schema: rateLimitCheckSchema }, async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');
    const parsed = parseJson(z.object({ key: z.string().min(1).max(120) }), request.query ?? {});
    const decision = deps.rateLimiter.hit(parsed.key);
    if (!decision.allowed) {
      throw new AppError(429, 'RATE_LIMIT_EXCEEDED', 'Too many requests');
    }
    return reply.send(decision);
  });

  // ── Server log stream (SSE) ─────────────────────────────────────────────
  app.get('/admin/logs/stream', async (request, reply) => {
    const user = await deps.authenticate(request);
    requireRole(user, 'admin');

    if (!deps.broadcaster) {
      throw new AppError(500, 'BROADCASTER_UNAVAILABLE', 'Log broadcaster not available');
    }

    const stream = deps.broadcaster.registerClient();
    return reply
      .header('Content-Type', 'text/event-stream')
      .header('Cache-Control', 'no-cache, no-store, must-revalidate')
      .header('Connection', 'keep-alive')
      .header('X-Accel-Buffering', 'no')
      .send(stream);
  });

  // ── Admin user management ──────────────────────────────────────────────────
  const listAdminUsersSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'List users (admin)',
    querystring: {
      type: 'object',
      properties: {
        limit: { type: 'integer', default: 20 },
        offset: { type: 'integer', default: 0 },
        role: { type: 'string', enum: ['customer', 'editor', 'admin'] },
        q: { type: 'string' },
      },
    },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.get('/admin/users', { schema: listAdminUsersSchema }, async (request, reply) => {
    const admin = await deps.authenticate(request);
    requireRole(admin, 'admin');
    const { limit, offset, role, q } = parseJson(userQuerySchema, request.query ?? {});
    // F-154: Users module == internal/backoffice only (never storefront customers).
    // Base literal excludes customers; ?role= then narrows within internal staff.
    const conditions: string[] = ["role <> 'customer'"];
    const values: unknown[] = [];
    let i = 1;
    if (role) {
      conditions.push(`role = $${i++}`);
      values.push(role);
    }
    if (q) {
      conditions.push(`(email ILIKE $${i++})`);
      values.push(`%${q}%`);
    }
    const where = `WHERE ${conditions.join(' AND ')}`;
    const countResult = await deps.pool.query<{ count: string }>(
      `SELECT COUNT(*) FROM identity_users ${where}`,
      values,
    );
    const total = parseInt(countResult.rows[0]?.count ?? '0', 10);
    const rows = await deps.pool.query(
      `SELECT id, email, role, created_at FROM identity_users ${where} ORDER BY created_at DESC LIMIT $${i++} OFFSET $${i}`,
      [...values, limit, offset],
    );
    return reply.send({
      items: rows.rows.map((r) => ({
        id: r.id,
        email: r.email,
        role: r.role,
        createdAt: r.created_at,
      })),
      total,
    });
  });

  const createAdminUserSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Create user (admin)',
    body: { type: 'object' },
    response: { 201: { type: 'object' }, 401: errorSchema, 403: errorSchema },
  };
  app.post('/admin/users', { schema: createAdminUserSchema }, async (request, reply) => {
    const admin = await deps.authenticate(request);
    requireRole(admin, 'admin');
    const input = parseJson(createUserBodySchema, request.body);
    const { default: argon2 } = await import('argon2');
    const passwordHash = await argon2.hash(input.password);
    try {
      const result = await deps.pool.query(
        `INSERT INTO identity_users (email, password_hash, role) VALUES ($1, $2, $3) RETURNING id, email, role, created_at`,
        [input.email.toLowerCase(), passwordHash, input.role],
      );
      const r = result.rows[0];
      return reply
        .code(201)
        .send({ id: r.id, email: r.email, role: r.role, createdAt: r.created_at });
    } catch (err) {
      if (isPgUniqueViolation(err)) {
        throw new AppError(409, 'EMAIL_EXISTS', 'Email already registered');
      }
      throw err;
    }
  });

  const patchAdminUserSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Update user (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    body: { type: 'object' },
    response: { 401: errorSchema, 403: errorSchema },
  };
  app.patch('/admin/users/:id', { schema: patchAdminUserSchema }, async (request, reply) => {
    const admin = await deps.authenticate(request);
    requireRole(admin, 'admin');
    const id = (request.params as { id: string }).id;
    const patch = parseJson(updateUserBodySchema, request.body);
    const sets: string[] = [];
    const values: unknown[] = [];
    let idx = 1;
    if (patch.role) {
      sets.push(`role = $${idx++}`);
      values.push(patch.role);
    }
    if (patch.password) {
      const { default: argon2 } = await import('argon2');
      sets.push(`password_hash = $${idx++}`);
      values.push(await argon2.hash(patch.password));
    }
    if (!sets.length) {
      const existing = await deps.pool.query(
        `SELECT id, email, role, created_at FROM identity_users WHERE id = $1`,
        [id],
      );
      if (!existing.rows[0]) throw new AppError(404, 'NOT_FOUND', 'User not found');
      return reply.send(existing.rows[0]);
    }
    values.push(id);
    const result = await deps.pool.query(
      `UPDATE identity_users SET ${sets.join(', ')} WHERE id = $${idx} RETURNING id, email, role, created_at`,
      values,
    );
    if (!result.rows[0]) throw new AppError(404, 'NOT_FOUND', 'User not found');
    return reply.send(result.rows[0]);
  });

  const deleteAdminUserSchema: FastifySchema = {
    tags: ['Admin'],
    summary: 'Delete user (admin)',
    params: {
      type: 'object',
      required: ['id'],
      properties: { id: { type: 'string', format: 'uuid' } },
    },
    response: { 204: { type: 'null' }, 401: errorSchema, 403: errorSchema },
  };
  app.delete('/admin/users/:id', { schema: deleteAdminUserSchema }, async (request, reply) => {
    const admin = await deps.authenticate(request);
    requireRole(admin, 'admin');
    const id = (request.params as { id: string }).id;
    if (admin.id === id)
      throw new AppError(400, 'CANNOT_DELETE_SELF', 'Cannot delete your own account');
    await deps.pool.query(`DELETE FROM identity_users WHERE id = $1`, [id]);
    return reply.code(204).send();
  });
}
