import { createHash, randomBytes, randomUUID } from 'node:crypto';
import bcrypt from 'bcryptjs';
import { Router } from 'express';
import { z } from 'zod';
import { createToken, requireAuth, type AuthenticatedRequest } from './auth';
import { pool } from './db';
import { config } from './config';
import { encryptSensitive } from './privacy';
import { generateAvailableUsername } from './usernames';

export const guardianQrRouter = Router();

const digest = (value: string): string => createHash('sha256').update(value).digest('hex');
const QR_PREFIX = 'ala-talabak:guardian:';
const QR_DISPLAY_LIFETIME_MS = 15_000;
const QR_OPEN_GRACE_INTERVAL = '2 minutes';

const qrPayload = (token: string): string => {
  const entry = new URL(config.guardianQrEntryUrl);
  entry.hash = new URLSearchParams({ token }).toString();
  return entry.toString();
};

const qrToken = (payload: string): string | null => {
  if (payload.startsWith(QR_PREFIX)) return payload.slice(QR_PREFIX.length);
  try {
    const entry = new URL(payload);
    if (entry.protocol === 'alatalabak:' && entry.hostname === 'guardian-login') {
      return entry.searchParams.get('token')
        ?? new URLSearchParams(entry.hash.slice(1)).get('token');
    }
    const expected = new URL(config.guardianQrEntryUrl);
    if (entry.origin !== expected.origin || entry.pathname !== expected.pathname) return null;
    return new URLSearchParams(entry.hash.slice(1)).get('token');
  } catch { return null; }
};

guardianQrRouter.post('/me/guardian-qr', requireAuth,
  async (request: AuthenticatedRequest, response) => {
    if (request.authUser?.role !== 'user' || request.authUser.accountType !== 'candidate') {
      response.status(403).json({ error: 'Candidate account required' }); return;
    }
    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      const exception = await client.query(
        `SELECT 1 FROM guardian_exceptions
         WHERE candidate_user_id=$1 AND status IN ('pending','approved')`,
        [request.authUser.id],
      );
      if (exception.rowCount) {
        await client.query('ROLLBACK');
        response.status(409).json({ error: 'No-guardian review is active' }); return;
      }
      const claimed = await client.query(
        `SELECT 1 FROM guardian_qr_challenges
         WHERE candidate_user_id=$1 AND status='claimed' AND claim_expires_at>NOW()
         LIMIT 1`,
        [request.authUser.id],
      );
      if (claimed.rowCount) {
        await client.query('ROLLBACK');
        response.status(409).json({ error: 'Guardian QR is already claimed' }); return;
      }
      await client.query(
        `UPDATE guardian_qr_challenges SET status='expired'
         WHERE candidate_user_id=$1 AND status='issued' AND expires_at<=NOW()`,
        [request.authUser.id],
      );
      const token = randomBytes(32).toString('base64url');
      const id = randomUUID();
      await client.query(
        `INSERT INTO guardian_qr_challenges
           (id,candidate_user_id,token_hash,expires_at)
         VALUES ($1,$2,$3,NOW()+$4::interval)`,
        [id, request.authUser.id, digest(token), QR_OPEN_GRACE_INTERVAL],
      );
      await client.query('COMMIT');
      response.status(201).json({
        challengeId: id,
        payload: qrPayload(token),
        expiresAt: new Date(Date.now() + QR_DISPLAY_LIFETIME_MS),
      });
    } catch (error) {
      await client.query('ROLLBACK'); throw error;
    } finally { client.release(); }
  });

guardianQrRouter.get('/me/guardian-qr/:id/status', requireAuth,
  async (request: AuthenticatedRequest, response) => {
    if (request.authUser?.role !== 'user' || request.authUser.accountType !== 'candidate') {
      response.status(403).json({ error: 'Candidate account required' }); return;
    }
    const id = z.uuid().safeParse(request.params.id);
    if (!id.success) { response.status(400).json({ error: 'Invalid QR challenge ID' }); return; }
    await pool.query(
      `UPDATE guardian_qr_challenges SET status='expired'
       WHERE id=$1 AND candidate_user_id=$2
         AND ((status='issued' AND expires_at<=NOW())
           OR (status='claimed' AND claim_expires_at<=NOW()))`,
      [id.data, request.authUser.id],
    );
    const result = await pool.query<{ status: 'issued' | 'claimed' | 'consumed' | 'expired' }>(
      `SELECT status FROM guardian_qr_challenges WHERE id=$1 AND candidate_user_id=$2`,
      [id.data, request.authUser.id],
    );
    if (!result.rows[0]) { response.status(404).json({ error: 'QR challenge not found' }); return; }
    response.json({ status: result.rows[0].status });
  });

const claimSchema = z.object({ payload: z.string().trim().min(20).max(1000) });

guardianQrRouter.post('/guardian-qr/claim', async (request, response) => {
  const parsed = claimSchema.safeParse(request.body);
  if (!parsed.success) { response.status(400).json({ error: 'Invalid QR payload' }); return; }
  const token = qrToken(parsed.data.payload);
  if (!token || !/^[A-Za-z0-9_-]{43}$/.test(token)) {
    response.status(400).json({ error: 'Invalid QR token' }); return;
  }
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const result = await client.query<{ id: string; candidate_user_id: string; candidate_name: string }>(
      `SELECT q.id,q.candidate_user_id,u.display_name AS candidate_name
       FROM guardian_qr_challenges q JOIN users u ON u.id=q.candidate_user_id AND u.is_active
       WHERE q.token_hash=$1 AND q.status='issued' AND q.expires_at>NOW()
       FOR UPDATE OF q`, [digest(token)],
    );
    const challenge = result.rows[0];
    if (!challenge) {
      await client.query('ROLLBACK');
      response.status(410).json({ error: 'QR code expired or already used' }); return;
    }
    await client.query(
      `UPDATE guardian_qr_challenges SET status='expired'
       WHERE candidate_user_id=$1 AND id<>$2 AND status='issued'`,
      [challenge.candidate_user_id, challenge.id],
    );
    const guardian = await client.query<{
      id: string; display_name: string; email: string | null; username: string | null;
      token_version: number; account_type: 'candidate' | 'guardian';
    }>(
      `SELECT u.id,u.display_name,u.email,u.username,u.token_version,u.account_type
       FROM guardian_invitations i JOIN users u ON u.id=i.accepted_by
       WHERE i.candidate_user_id=$1 AND i.status IN ('registered','verified')
         AND u.account_type IN ('candidate','guardian') AND u.is_active AND u.deleted_at IS NULL
       ORDER BY i.created_at DESC LIMIT 1`, [challenge.candidate_user_id],
    );
    if (guardian.rows[0]) {
      await client.query(
        `UPDATE guardian_qr_challenges SET status='consumed',claimed_at=NOW(),consumed_at=NOW()
         WHERE id=$1`, [challenge.id],
      );
      await client.query(
        "INSERT INTO audit_events (user_id,event_type,details) VALUES ($1,'guardian_qr_login',$2::jsonb)",
        [guardian.rows[0].id, JSON.stringify({ candidateUserId: challenge.candidate_user_id })],
      );
      await client.query('COMMIT');
      const user = {
        id: guardian.rows[0].id, email: guardian.rows[0].email, username: guardian.rows[0].username,
        displayName: guardian.rows[0].display_name,
        role: 'user' as const, accountType: guardian.rows[0].account_type,
      };
      response.json({ kind: 'login', token: createToken(user, guardian.rows[0].token_version), user });
      return;
    }
    const enrollmentToken = randomBytes(32).toString('base64url');
    await client.query(
      `UPDATE guardian_qr_challenges SET status='claimed',claimed_at=NOW(),
         claim_hash=$2,claim_expires_at=NOW()+INTERVAL '5 minutes'
       WHERE id=$1`,
      [challenge.id, digest(enrollmentToken)],
    );
    await client.query('COMMIT');
    response.json({ kind: 'enroll', enrollmentToken, candidateName: challenge.candidate_name });
  } catch (error) {
    await client.query('ROLLBACK'); throw error;
  } finally { client.release(); }
});

const releaseSchema = z.object({
  enrollmentToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
});

guardianQrRouter.post('/guardian-qr/release', async (request, response) => {
  const parsed = releaseSchema.safeParse(request.body);
  if (!parsed.success) { response.status(400).json({ error: 'Invalid guardian enrollment token' }); return; }
  const result = await pool.query(
    `UPDATE guardian_qr_challenges
     SET status='expired',claim_hash=NULL,claim_expires_at=NULL
     WHERE claim_hash=$1 AND status='claimed'
     RETURNING id`,
    [digest(parsed.data.enrollmentToken)],
  );
  response.json({ released: Boolean(result.rowCount) });
});

const enrollmentSchema = z.object({
  enrollmentToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
  fullName: z.string().trim().min(4).max(120),
  phone: z.string().trim().regex(/^\+[1-9]\d{7,14}$/),
  password: z.string().min(8).max(128),
  passwordConfirmation: z.string().min(8).max(128),
}).refine((value) => value.password === value.passwordConfirmation, {
  path: ['passwordConfirmation'], message: 'Passwords do not match',
});

guardianQrRouter.post('/guardian-qr/enroll', async (request, response) => {
  const parsed = enrollmentSchema.safeParse(request.body);
  if (!parsed.success) { response.status(400).json({ error: 'Invalid guardian profile' }); return; }
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const result = await client.query<{ id: string; candidate_user_id: string }>(
      `SELECT id,candidate_user_id FROM guardian_qr_challenges
       WHERE claim_hash=$1 AND status='claimed' AND claim_expires_at>NOW()
       FOR UPDATE`, [digest(parsed.data.enrollmentToken)],
    );
    const challenge = result.rows[0];
    if (!challenge) {
      await client.query('ROLLBACK');
      response.status(410).json({ error: 'Guardian enrollment expired or already used' }); return;
    }
    await client.query('SELECT pg_advisory_xact_lock(hashtext($1))',
      [`guardian-enroll:${challenge.candidate_user_id}`]);
    const existing = await client.query(
      `SELECT 1 FROM guardian_invitations WHERE candidate_user_id=$1
       AND status IN ('registered','verified')`, [challenge.candidate_user_id],
    );
    const exception = await client.query(
      `SELECT 1 FROM guardian_exceptions WHERE candidate_user_id=$1
       AND status IN ('pending','approved')`, [challenge.candidate_user_id],
    );
    if (existing.rowCount || exception.rowCount) {
      await client.query('ROLLBACK');
      response.status(409).json({ error: 'Guardian enrollment is no longer available' }); return;
    }
    await client.query(
      `UPDATE guardian_invitations SET status='revoked',completed_at=NOW()
       WHERE candidate_user_id=$1 AND status='pending'`, [challenge.candidate_user_id],
    );
    await client.query(
      `DELETE FROM guardian_contact_hashes h USING guardian_invitations i
       WHERE h.invitation_id=i.id AND i.candidate_user_id=$1 AND i.status='revoked'`,
      [challenge.candidate_user_id],
    );
    const guardianId = randomUUID();
    const displayName = parsed.data.fullName;
    const username = await generateAvailableUsername(client, displayName);
    await client.query(
      `INSERT INTO users (id,email,password_hash,display_name,username,role,account_type)
       VALUES ($1,$2,$3,$4,$5,'user','guardian')`,
      [guardianId, `${username}@guardian.local`,
        await bcrypt.hash(parsed.data.password, config.passwordHashRounds), displayName, username],
    );
    const invitationId = randomUUID();
    await client.query(
      `INSERT INTO guardian_invitations
         (id,candidate_user_id,code_hash,status,accepted_by,expires_at,phone_encrypted,phone_last_four)
       VALUES ($1,$2,$3,'registered',$4,NOW()+INTERVAL '7 days',$5,$6)`,
      [invitationId, challenge.candidate_user_id,
        digest(randomBytes(32).toString('base64url')), guardianId,
        encryptSensitive(parsed.data.phone), parsed.data.phone.slice(-4)],
    );
    await client.query(
      `UPDATE guardian_qr_challenges SET status='consumed',consumed_at=NOW()
       WHERE id=$1`, [challenge.id],
    );
    await client.query(
      "INSERT INTO audit_events (user_id,event_type,details) VALUES ($1,'guardian_qr_enrolled',$2::jsonb)",
      [guardianId, JSON.stringify({ candidateUserId: challenge.candidate_user_id, invitationId })],
    );
    await client.query('COMMIT');
    const user = {
      id: guardianId, email: `${username}@guardian.local`, username,
      displayName, role: 'user' as const, accountType: 'guardian' as const,
    };
    response.status(201).json({ kind: 'enrolled', token: createToken(user, 0), user });
  } catch (error) {
    await client.query('ROLLBACK'); throw error;
  } finally { client.release(); }
});

const existingGuardianSchema = z.object({
  enrollmentToken: z.string().regex(/^[A-Za-z0-9_-]{43}$/),
  phone: z.string().trim().regex(/^\+[1-9]\d{7,14}$/),
  confirmed: z.literal(true),
});

guardianQrRouter.post('/guardian-qr/link-existing', requireAuth,
  async (request: AuthenticatedRequest, response) => {
    if (request.authUser?.role !== 'user' || request.authUser.accountType !== 'candidate') {
      response.status(403).json({ error: 'Candidate account required' }); return;
    }
    const parsed = existingGuardianSchema.safeParse(request.body);
    if (!parsed.success) { response.status(400).json({ error: 'Explicit guardian confirmation is required' }); return; }
    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      const challengeResult = await client.query<{ id: string; candidate_user_id: string }>(
        `SELECT id,candidate_user_id FROM guardian_qr_challenges
         WHERE claim_hash=$1 AND status='claimed' AND claim_expires_at>NOW()
         FOR UPDATE`,
        [digest(parsed.data.enrollmentToken)],
      );
      const challenge = challengeResult.rows[0];
      if (!challenge) {
        await client.query('ROLLBACK');
        response.status(410).json({ error: 'Guardian enrollment expired or already used' }); return;
      }
      if (challenge.candidate_user_id === request.authUser.id) {
        await client.query('ROLLBACK');
        response.status(409).json({ error: 'An account cannot become its own guardian' }); return;
      }
      await client.query('SELECT pg_advisory_xact_lock(hashtext($1))',
        [`guardian-enroll:${challenge.candidate_user_id}`]);
      const unavailable = await client.query(
        `SELECT 1 FROM guardian_invitations WHERE candidate_user_id=$1
         AND status IN ('registered','verified')
         UNION ALL
         SELECT 1 FROM guardian_exceptions WHERE candidate_user_id=$1
         AND status IN ('pending','approved')`,
        [challenge.candidate_user_id],
      );
      if (unavailable.rowCount) {
        await client.query('ROLLBACK');
        response.status(409).json({ error: 'Guardian enrollment is no longer available' }); return;
      }

      await client.query(
        `UPDATE guardian_invitations SET status='revoked',completed_at=NOW()
         WHERE candidate_user_id=$1 AND status='pending'`,
        [challenge.candidate_user_id],
      );
      const invitationId = randomUUID();
      await client.query(
        `INSERT INTO guardian_invitations
           (id,candidate_user_id,code_hash,status,accepted_by,expires_at,phone_encrypted,phone_last_four)
         VALUES ($1,$2,$3,'registered',$4,NOW()+INTERVAL '7 days',$5,$6)`,
        [invitationId, challenge.candidate_user_id,
          digest(randomBytes(32).toString('base64url')), request.authUser.id,
          encryptSensitive(parsed.data.phone), parsed.data.phone.slice(-4)],
      );
      const current = await client.query<{
        id: string; email: string | null; username: string | null;
        display_name: string; token_version: number; account_type: 'candidate';
      }>(
        `SELECT id,email,username,display_name,token_version,account_type
         FROM users WHERE id=$1 AND is_active AND deleted_at IS NULL AND account_type='candidate'`,
        [request.authUser.id],
      );
      if (!current.rows[0]) {
        await client.query('ROLLBACK'); response.status(409).json({ error: 'Account is no longer available' }); return;
      }
      await client.query(
        `UPDATE guardian_qr_challenges SET status='consumed',consumed_at=NOW() WHERE id=$1`,
        [challenge.id],
      );
      await client.query(
        "INSERT INTO audit_events (user_id,event_type,details) VALUES ($1,'candidate_linked_as_guardian',$2::jsonb)",
        [request.authUser.id, JSON.stringify({
          candidateUserId: challenge.candidate_user_id, invitationId,
        })],
      );
      await client.query('COMMIT');
      const row = current.rows[0];
      const user = {
        id: row.id, email: row.email, username: row.username,
        displayName: row.display_name, role: 'user' as const, accountType: 'candidate' as const,
      };
      response.status(201).json({ token: createToken(user, row.token_version), user });
    } catch (error) {
      await client.query('ROLLBACK'); throw error;
    } finally { client.release(); }
  });
