import { createHash, randomBytes, randomUUID } from 'node:crypto';
import { Router, type Response } from 'express';
import { z } from 'zod';
import { requireAuth, type AuthenticatedRequest } from './auth';
import { pool } from './db';
import { recordJourneyEvent } from './relationshipEvents';
import { autoAssignJourneyGuardians } from './autoGuardianSelection';
import { journeyNotificationRecipients, notifyUsers } from './notifications';
import { ensurePaymentObligations } from './paymentService';

export const familyJourneyRouter = Router();

interface Access {
  id: string;
  status: 'active' | 'closed';
  first_user_id: string;
  second_user_id: string;
  permission: 'participant' | 'viewer' | 'contributor' | null;
}

const readAccess = async (journeyId: string, actorId: string): Promise<Access | null> => {
  const result = await pool.query<Access>(
    `SELECT j.id, j.status, j.first_user_id, j.second_user_id,
       CASE WHEN j.first_user_id=$2 OR j.second_user_id=$2 THEN 'participant'
         ELSE (SELECT m.permission FROM journey_memberships m
               WHERE m.journey_id=j.id AND m.user_id=$2 AND m.is_active) END AS permission
     FROM journeys j WHERE j.id=$1`,
    [journeyId, actorId],
  );
  const access = result.rows[0];
  return access?.permission ? access : null;
};

export const authorizeJourney = async (
  request: AuthenticatedRequest,
  response: Response,
  level: 'read' | 'write' | 'participant' | 'participantRead',
): Promise<Access | null> => {
  const id = z.uuid().safeParse(request.params.id);
  if (!id.success) {
    response.status(400).json({ error: 'Invalid journey ID' });
    return null;
  }
  const access = await readAccess(id.data, request.authUser!.id);
  if (request.authUser!.accountType !== 'candidate') {
    response.status(403).json({ error: 'Family accounts use the restricted call view' });
    return null;
  }
  if (access?.permission !== 'participant') {
    const selectedGuardian = await pool.query(
      `SELECT 1 FROM journey_call_guardians WHERE journey_id=$1
       AND guardian_user_id=$2`,
      [id.data, request.authUser!.id],
    );
    if (selectedGuardian.rowCount) {
      response.status(403).json({ error: 'Guardian uses the restricted call view' });
      return null;
    }
  }
  if (!access || ((level === 'participant' || level === 'participantRead') && access.permission !== 'participant')
    || (level === 'write' && access.permission === 'viewer')) {
    response.status(404).json({ error: 'Journey not found for this account' });
    return null;
  }
  if (level !== 'read' && level !== 'participantRead' && access.status !== 'active') {
    response.status(409).json({ error: 'Journey is closed' });
    return null;
  }
  return access;
};

const inviteSchema = z.object({
  roleLabel: z.string().trim().min(2).max(80),
  permission: z.enum(['viewer', 'contributor']),
});

familyJourneyRouter.post('/journeys/:id/invitations', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'participant');
  if (!access) return;
  const parsed = inviteSchema.safeParse(request.body);
  if (!parsed.success) {
    response.status(400).json({ error: 'Invalid invitation data' });
    return;
  }
  const code = randomBytes(24).toString('base64url');
  const codeHash = createHash('sha256').update(code).digest('hex');
  const id = randomUUID();
  const result = await pool.query(
    `INSERT INTO family_invitations
       (id, journey_id, invited_by, code_hash, role_label, permission, expires_at)
     VALUES ($1, $2, $3, $4, $5, $6, NOW()+INTERVAL '7 days')
     RETURNING id, role_label AS "roleLabel", permission, expires_at AS "expiresAt"`,
    [id, access.id, request.authUser!.id, codeHash, parsed.data.roleLabel, parsed.data.permission],
  );
  response.status(201).json({ invitation: result.rows[0], code });
});

familyJourneyRouter.get('/journeys/:id/invitations', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'participant');
  if (!access) return;
  const result = await pool.query(
    `SELECT i.id, i.role_label AS "roleLabel", i.permission, i.status,
       i.expires_at AS "expiresAt", i.created_at AS "createdAt",
       u.display_name AS "acceptedByName", i.invited_by AS "invitedBy"
     FROM family_invitations i LEFT JOIN users u ON u.id=i.accepted_by
     WHERE i.journey_id=$1 ORDER BY i.created_at DESC LIMIT 50`,
    [access.id],
  );
  response.json({ invitations: result.rows });
});

familyJourneyRouter.post('/journeys/:id/invitations/:invitationId/revoke', requireAuth,
  async (request: AuthenticatedRequest, response) => {
    const access = await authorizeJourney(request, response, 'participant');
    const invitationId = z.uuid().safeParse(request.params.invitationId);
    if (!access || !invitationId.success) return;
    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      const result = await client.query(
        `UPDATE family_invitations SET status='revoked'
         WHERE id=$1 AND journey_id=$2 AND invited_by=$3 AND status<>'revoked'
         RETURNING id`,
        [invitationId.data, access.id, request.authUser!.id],
      );
      if (!result.rowCount) {
        await client.query('ROLLBACK');
        response.status(404).json({ error: 'Active invitation not found' });
        return;
      }
      await client.query('UPDATE journey_memberships SET is_active=FALSE WHERE invitation_id=$1', [invitationId.data]);
      await client.query(
        "INSERT INTO audit_events (user_id, event_type, details) VALUES ($1, 'family_invitation_revoked', $2::jsonb)",
        [request.authUser!.id, JSON.stringify({ invitationId: invitationId.data, journeyId: access.id })],
      );
      await client.query('COMMIT');
      response.json({ revoked: true });
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally { client.release(); }
  });

familyJourneyRouter.post('/invitations/accept', requireAuth, async (request: AuthenticatedRequest, response) => {
  const parsed = z.object({ code: z.string().min(20).max(100) }).safeParse(request.body);
  if (!parsed.success || request.authUser?.role !== 'user'
    || request.authUser.accountType !== 'candidate') {
    response.status(400).json({ error: 'Invalid invitation code' });
    return;
  }
  const codeHash = createHash('sha256').update(parsed.data.code).digest('hex');
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const match = await client.query<{
      id: string; journey_id: string; role_label: string; permission: string;
      first_user_id: string; second_user_id: string; journey_status: string;
    }>(
      `SELECT i.id, i.journey_id, i.role_label, i.permission,
         j.first_user_id, j.second_user_id, j.status AS journey_status
       FROM family_invitations i JOIN journeys j ON j.id=i.journey_id
       WHERE i.code_hash=$1 AND i.status='pending' AND i.expires_at>NOW()
       FOR UPDATE OF i`,
      [codeHash],
    );
    const invitation = match.rows[0];
    if (!invitation || invitation.journey_status !== 'active'
      || invitation.first_user_id === request.authUser!.id
      || invitation.second_user_id === request.authUser!.id) {
      await client.query('ROLLBACK');
      response.status(404).json({ error: 'Invitation is unavailable or expired' });
      return;
    }
    const existing = await client.query(
      'SELECT 1 FROM journey_memberships WHERE journey_id=$1 AND user_id=$2',
      [invitation.journey_id, request.authUser!.id],
    );
    if (existing.rowCount) {
      await client.query('ROLLBACK');
      response.status(409).json({ error: 'This account already participated in this journey' });
      return;
    }
    await client.query(
      `INSERT INTO journey_memberships (journey_id, user_id, invitation_id, role_label, permission)
       VALUES ($1, $2, $3, $4, $5)`,
      [invitation.journey_id, request.authUser!.id, invitation.id,
        invitation.role_label, invitation.permission],
    );
    await client.query(
      `UPDATE family_invitations SET status='accepted', accepted_by=$1, accepted_at=NOW()
       WHERE id=$2`,
      [request.authUser!.id, invitation.id],
    );
    await client.query(
      "INSERT INTO audit_events (user_id, event_type, details) VALUES ($1, 'family_invitation_accepted', $2::jsonb)",
      [request.authUser!.id, JSON.stringify({ invitationId: invitation.id, journeyId: invitation.journey_id })],
    );
    await client.query('COMMIT');
    response.json({ journeyId: invitation.journey_id });
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally { client.release(); }
});

familyJourneyRouter.get('/journeys/:id/room', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'read');
  if (!access) return;
  const [names, members, messages] = await Promise.all([
    pool.query(
      `SELECT u.id, p.display_name AS "displayName" FROM users u JOIN profiles p ON p.user_id=u.id
       WHERE u.id = ANY($1::uuid[])`, [[access.first_user_id, access.second_user_id]],
    ),
    pool.query(
      `SELECT m.user_id AS "userId", u.display_name AS "displayName", m.role_label AS "roleLabel",
              m.permission, m.joined_at AS "joinedAt", fi.invited_by AS "invitedBy"
       FROM journey_memberships m JOIN users u ON u.id=m.user_id
       JOIN family_invitations fi ON fi.id=m.invitation_id
       WHERE m.journey_id=$1 AND m.is_active ORDER BY m.joined_at`, [access.id],
    ),
    pool.query(
      `SELECT m.id, m.author_user_id AS "authorUserId", u.display_name AS "authorName",
              m.body, m.created_at AS "createdAt"
       FROM journey_messages m JOIN users u ON u.id=m.author_user_id
       WHERE m.journey_id=$1 ORDER BY m.created_at DESC, m.id DESC LIMIT 100`, [access.id],
    ),
  ]);
  response.json({ status: access.status, permission: access.permission,
    participants: names.rows, members: members.rows, messages: messages.rows.reverse() });
});

familyJourneyRouter.post('/journeys/:id/messages', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'write');
  if (!access) return;
  const parsed = z.object({ body: z.string().trim().min(1).max(2000), clientNonce: z.uuid() })
    .safeParse(request.body);
  if (!parsed.success) {
    response.status(400).json({ error: 'Invalid message' });
    return;
  }
  const result = await pool.query(
    `INSERT INTO journey_messages (id, journey_id, author_user_id, body, client_nonce)
     VALUES ($1, $2, $3, $4, $5)
     ON CONFLICT (journey_id, author_user_id, client_nonce)
     DO UPDATE SET body=journey_messages.body
     RETURNING id, author_user_id AS "authorUserId", body, created_at AS "createdAt"`,
    [randomUUID(), access.id, request.authUser!.id, parsed.data.body, parsed.data.clientNonce],
  );
  response.status(201).json({ message: { ...result.rows[0], authorName: request.authUser!.displayName } });
});

familyJourneyRouter.post('/journeys/:id/close', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'participant');
  if (!access) return;
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    await client.query("UPDATE journeys SET status='closed', updated_at=NOW() WHERE id=$1", [access.id]);
    await client.query('DELETE FROM journey_contact_grants WHERE journey_id=$1', [access.id]);
    await client.query("UPDATE family_invitations SET status='revoked' WHERE journey_id=$1 AND status<>'revoked'", [access.id]);
    await client.query('UPDATE journey_memberships SET is_active=FALSE WHERE journey_id=$1', [access.id]);
    await client.query(
      "UPDATE decision_rounds SET status='resolved', outcome='closed', resolved_at=NOW() WHERE journey_id=$1 AND status='open'",
      [access.id],
    );
    await client.query(
      "INSERT INTO audit_events (user_id, event_type, details) VALUES ($1, 'journey_closed', $2::jsonb)",
      [request.authUser!.id, JSON.stringify({ journeyId: access.id })],
    );
    await client.query('COMMIT');
    response.json({ closed: true });
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally { client.release(); }
});

const meetingSchema = z.object({ startsAt: z.iso.datetime({ offset: true }), timeZone: z.string().min(1).max(80) });

familyJourneyRouter.get('/journeys/:id/meetings', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'read');
  if (!access) return;
  const result = await pool.query(
    `SELECT m.id, m.proposed_by AS "proposedBy", m.starts_at AS "startsAt",
            m.time_zone AS "timeZone", m.status, m.video_required AS "videoRequired",
            m.created_at AS "createdAt", m.session_number AS "sessionNumber",
            (SELECT COUNT(*)::int FROM meeting_attendance a WHERE a.meeting_id=m.id) AS "attendanceCount",
            EXISTS (SELECT 1 FROM meeting_attendance a WHERE a.meeting_id=m.id AND a.user_id=$2) AS "iAttended"
     FROM meeting_proposals m WHERE m.journey_id=$1 ORDER BY m.created_at DESC LIMIT 30`,
    [access.id, request.authUser!.id],
  );
  response.json({ meetings: result.rows });
});

familyJourneyRouter.post('/journeys/:id/meetings', requireAuth, async (request: AuthenticatedRequest, response) => {
  const access = await authorizeJourney(request, response, 'participant');
  if (!access) return;
  const parsed = meetingSchema.safeParse(request.body);
  if (!parsed.success || Date.parse(parsed.data.startsAt) <= Date.now()) {
    response.status(400).json({ error: 'Meeting time must be in the future' });
    return;
  }
  try { new Intl.DateTimeFormat('en', { timeZone: parsed.data.timeZone }); }
  catch { response.status(400).json({ error: 'Invalid time zone' }); return; }
  try {
    const completed = await pool.query<{ count: number }>(
      `SELECT COUNT(*)::int AS count FROM meeting_proposals
       WHERE journey_id=$1 AND status='completed'`, [access.id],
    );
    const sessionNumber = completed.rows[0].count + 1;
    if (sessionNumber > 3) {
      response.status(409).json({ error: 'The maximum of three sessions has been reached' }); return;
    }
    const result = await pool.query(
      `INSERT INTO meeting_proposals (id, journey_id, proposed_by, starts_at, time_zone, video_required, session_number)
       VALUES ($1, $2, $3, $4, $5, TRUE, $6)
       RETURNING id, starts_at AS "startsAt", time_zone AS "timeZone", status`,
      [randomUUID(), access.id, request.authUser!.id, parsed.data.startsAt, parsed.data.timeZone, sessionNumber],
    );
    await recordJourneyEvent(access.id, request.authUser!.id, 'meeting_proposed', {
      meetingId: result.rows[0].id, startsAt: parsed.data.startsAt, timeZone: parsed.data.timeZone,
    });
    const recipientId = access.first_user_id === request.authUser!.id
      ? access.second_user_id : access.first_user_id;
    await notifyUsers([recipientId], {
      type: 'meeting_proposed', title: 'اقتراح موعد جديد',
      body: `${request.authUser!.displayName} اقترح موعدًا لمكالمة الفيديو.`,
      journeyId: access.id, dedupeKey: `meeting-proposed:${result.rows[0].id}`,
    });
    response.status(201).json({ meeting: result.rows[0] });
  } catch (error) {
    if (typeof error === 'object' && error !== null && 'code' in error && error.code === '23505') {
      response.status(409).json({ error: 'An active meeting already exists' });
      return;
    }
    throw error;
  }
});

familyJourneyRouter.post('/journeys/:id/meetings/:meetingId/confirm', requireAuth,
  async (request: AuthenticatedRequest, response) => {
    const access = await authorizeJourney(request, response, 'participant');
    const meetingId = z.uuid().safeParse(request.params.meetingId);
    if (!access || !meetingId.success) return;
    const result = await pool.query(
      `UPDATE meeting_proposals SET status='confirmed'
       WHERE id=$1 AND journey_id=$2 AND proposed_by<>$3 AND status='proposed'
       RETURNING id, status`,
      [meetingId.data, access.id, request.authUser!.id],
    );
    if (!result.rowCount) {
      response.status(404).json({ error: 'Meeting proposal not found for confirmation' });
      return;
    }
    await recordJourneyEvent(access.id, request.authUser!.id, 'meeting_confirmed', {
      meetingId: meetingId.data,
    });
    await autoAssignJourneyGuardians(access.id);
    await ensurePaymentObligations(meetingId.data);
    const recipients = await journeyNotificationRecipients(access.id, request.authUser!.id);
    await notifyUsers(recipients.userIds, {
      type: 'meeting_confirmed', title: 'تم الاتفاق على الموعد',
      body: 'وافق الطرفان على الموعد وأصبح جاهزًا لتأكيد وليّي الأمر.',
      journeyId: access.id, requestId: recipients.requestId,
      dedupeKey: `meeting-confirmed:${meetingId.data}`,
    });
    response.json({ meeting: result.rows[0] });
  });

familyJourneyRouter.post('/journeys/:id/meetings/:meetingId/replace', requireAuth,
  async (request: AuthenticatedRequest, response) => {
    const access = await authorizeJourney(request, response, 'participant');
    const meetingId = z.uuid().safeParse(request.params.meetingId);
    const parsed = meetingSchema.safeParse(request.body);
    if (!access || !meetingId.success || !parsed.success
      || Date.parse(parsed.data.startsAt) <= Date.now()) {
      if (access) response.status(400).json({ error: 'Invalid replacement meeting' });
      return;
    }
    try { new Intl.DateTimeFormat('en', { timeZone: parsed.data.timeZone }); }
    catch { response.status(400).json({ error: 'Invalid time zone' }); return; }
    const client = await pool.connect();
    try {
      await client.query('BEGIN');
      const cancelled = await client.query<{ id: string; session_number: number }>(
        `UPDATE meeting_proposals SET status='cancelled'
         WHERE id=$1 AND journey_id=$2 AND status IN ('proposed','confirmed') RETURNING id,session_number`,
        [meetingId.data, access.id],
      );
      if (!cancelled.rowCount) {
        await client.query('ROLLBACK'); response.status(409).json({ error: 'Meeting cannot be replaced' }); return;
      }
      await client.query('DELETE FROM meeting_schedule_confirmations WHERE meeting_id=$1', [meetingId.data]);
      await client.query(
        `UPDATE meeting_call_sessions SET left_at=NOW()
         WHERE meeting_id=$1 AND left_at IS NULL`, [meetingId.data],
      );
      const created = await client.query<{ id: string }>(
        `INSERT INTO meeting_proposals (id,journey_id,proposed_by,starts_at,time_zone,video_required,session_number)
         VALUES ($1,$2,$3,$4,$5,TRUE,$6) RETURNING id`,
        [randomUUID(), access.id, request.authUser!.id, parsed.data.startsAt, parsed.data.timeZone,
          cancelled.rows[0].session_number],
      );
      await client.query('COMMIT');
      await recordJourneyEvent(access.id, request.authUser!.id, 'meeting_cancelled', {
        meetingId: meetingId.data, replaced: true,
      });
      await recordJourneyEvent(access.id, request.authUser!.id, 'meeting_proposed', {
        meetingId: created.rows[0].id, startsAt: parsed.data.startsAt, timeZone: parsed.data.timeZone,
      });
      const recipientId = access.first_user_id === request.authUser!.id
        ? access.second_user_id : access.first_user_id;
      await notifyUsers([recipientId], {
        type: 'meeting_proposed', title: 'اقتراح موعد بديل',
        body: `${request.authUser!.displayName} اقترح موعدًا جديدًا لمكالمة الفيديو.`,
        journeyId: access.id, dedupeKey: `meeting-proposed:${created.rows[0].id}`,
      });
      response.status(201).json({ meetingId: created.rows[0].id });
    } catch (error) { await client.query('ROLLBACK'); throw error; }
    finally { client.release(); }
  });
