import { pool } from './db';
import {
  listLiveKitParticipants,
  listLiveKitRooms,
  liveKitReady,
  removeLiveKitParticipant,
  endLiveKitRoom,
} from './liveKitMedia';
import { recordJourneyEvent } from './relationshipEvents';

const SOLO_LIMIT_MS = 10 * 60_000;
const MONITOR_INTERVAL_MS = 15_000;
const ROOM_PREFIX = 'family-';

interface ParticipantMetadata {
  sessionId?: string;
  userId?: string;
  observer?: boolean;
}

const readMetadata = (metadata: string): ParticipantMetadata => {
  try {
    return JSON.parse(metadata || '{}') as ParticipantMetadata;
  } catch {
    return {};
  }
};

const clearSoloState = async (meetingId: string): Promise<void> => {
  await pool.query(
    `INSERT INTO meeting_call_controls (meeting_id, solo_identity, solo_since, updated_at)
     SELECT $1,NULL,NULL,NOW() WHERE EXISTS (SELECT 1 FROM meeting_proposals WHERE id=$1)
     ON CONFLICT (meeting_id) DO UPDATE
       SET solo_identity=NULL,solo_since=NULL,updated_at=NOW()`,
    [meetingId],
  );
};

const recordEmptyState = async (meetingId: string): Promise<Date | null> => {
  const result = await pool.query<{ empty_since: Date }>(
    `INSERT INTO meeting_call_controls (meeting_id,solo_identity,solo_since,empty_since,updated_at)
     SELECT $1,NULL,NULL,NOW(),NOW() WHERE EXISTS (
       SELECT 1 FROM meeting_proposals meeting WHERE meeting.id=$1
       AND meeting.status='confirmed' AND (
         SELECT COUNT(DISTINCT session.user_id) FROM meeting_call_sessions session
         JOIN meeting_call_links link ON link.meeting_id=session.meeting_id
           AND session.session_id IN (link.lower_session_id,link.upper_session_id)
           AND link.lower_confirmed AND link.upper_confirmed
         WHERE session.meeting_id=meeting.id)=4)
     ON CONFLICT (meeting_id) DO UPDATE SET
       solo_identity=NULL,solo_since=NULL,
       empty_since=COALESCE(meeting_call_controls.empty_since,NOW()),updated_at=NOW()
     RETURNING empty_since`, [meetingId],
  );
  return result.rows[0]?.empty_since ?? null;
};

const clearEmptyState = async (meetingId: string): Promise<void> => {
  await pool.query(
    'UPDATE meeting_call_controls SET empty_since=NULL,updated_at=NOW() WHERE meeting_id=$1',
    [meetingId],
  );
};

const completeEmptyMeeting = async (meetingId: string): Promise<void> => {
  const result = await pool.query<{ journey_id: string }>(
    `UPDATE meeting_proposals SET status='completed',call_ended_at=COALESCE(call_ended_at,NOW())
     WHERE id=$1 AND status='confirmed' RETURNING journey_id`, [meetingId],
  );
  if (result.rows[0]) {
    await recordJourneyEvent(result.rows[0].journey_id, null, 'meeting_completed', { meetingId });
  }
};

const completeExpiredEmptyMeetings = async (): Promise<void> => {
  const result = await pool.query<{ id: string }>(
    `SELECT meeting.id
     FROM meeting_proposals meeting
     JOIN meeting_call_controls controls ON controls.meeting_id=meeting.id
     WHERE meeting.status='confirmed'
       AND controls.empty_since IS NOT NULL
       AND controls.empty_since <= NOW() - INTERVAL '10 minutes'`,
  );
  for (const meeting of result.rows) await completeEmptyMeeting(meeting.id);
};

const recordSoloState = async (meetingId: string, identity: string): Promise<Date> => {
  const result = await pool.query<{ solo_since: Date }>(
    `INSERT INTO meeting_call_controls (meeting_id, solo_identity, solo_since, updated_at)
     VALUES ($1,$2,NOW(),NOW())
     ON CONFLICT (meeting_id) DO UPDATE SET
       solo_since=CASE
         WHEN meeting_call_controls.solo_identity=EXCLUDED.solo_identity
           THEN meeting_call_controls.solo_since
         ELSE NOW()
       END,
       solo_identity=EXCLUDED.solo_identity,
       updated_at=NOW()
     RETURNING solo_since`,
    [meetingId, identity],
  );
  return result.rows[0].solo_since;
};

const markSoloTimeout = async (
  meetingId: string,
  metadata: ParticipantMetadata,
): Promise<void> => {
  if (metadata.sessionId) {
    await pool.query(
      `UPDATE meeting_call_sessions
       SET left_at=COALESCE(left_at,NOW()), forced_reason='solo_timeout'
       WHERE meeting_id=$1 AND session_id=$2`,
      [meetingId, metadata.sessionId],
    );
  }
  await pool.query(
    `INSERT INTO audit_events (user_id,event_type,details)
     VALUES ($1,'family_call_solo_timeout',$2::jsonb)`,
    [metadata.userId ?? null, JSON.stringify({ meetingId, limitMinutes: 10 })],
  );
  await clearSoloState(meetingId);
};

export const enforceCallLifecycle = async (): Promise<void> => {
  if (!liveKitReady()) return;
  const rooms = await listLiveKitRooms();
  for (const room of rooms) {
    if (!room.name.startsWith(ROOM_PREFIX)) continue;
    const meetingId = room.name.slice(ROOM_PREFIX.length);
    const meetingExists = await pool.query('SELECT 1 FROM meeting_proposals WHERE id=$1', [meetingId]);
    if (!meetingExists.rowCount) {
      await endLiveKitRoom(meetingId).catch(() => undefined);
      continue;
    }
    const participants = await listLiveKitParticipants(room.name);
    const described = participants.map((participant) => ({
      identity: participant.identity,
      metadata: readMetadata(participant.metadata),
    }));
    const regular = described.filter((participant) => !participant.metadata.observer);
    const observers = described.filter((participant) => participant.metadata.observer);

    if (regular.length === 0) {
      for (const observer of observers) {
        await removeLiveKitParticipant(room.name, observer.identity).catch(() => undefined);
      }
      const emptySince = await recordEmptyState(meetingId);
      if (emptySince && Date.now() - emptySince.getTime() >= SOLO_LIMIT_MS) {
        await completeEmptyMeeting(meetingId);
      }
      continue;
    }
    await clearEmptyState(meetingId);
    if (regular.length > 1) {
      await clearSoloState(meetingId);
      continue;
    }

    const solo = regular[0];
    const soloSince = await recordSoloState(meetingId, solo.identity);
    if (Date.now() - soloSince.getTime() < SOLO_LIMIT_MS) continue;
    await removeLiveKitParticipant(room.name, solo.identity);
      await markSoloTimeout(meetingId, solo.metadata);
  }
  await completeExpiredEmptyMeetings();
};

export const startCallLifecycleMonitor = (): NodeJS.Timeout | null => {
  if (!liveKitReady()) return null;
  const timer = setInterval(() => {
    void enforceCallLifecycle().catch((error: unknown) => {
      console.error('Family call lifecycle check failed', error);
    });
  }, MONITOR_INTERVAL_MS);
  timer.unref();
  return timer;
};
