import { Router, urlencoded } from 'express';
import jwt from 'jsonwebtoken';
import { authenticateToken } from '../middleware/auth.js';
import { getDb } from '../config/database.js';
import { log } from '../config/logger.js';
import { isTranscriptionEnabled, requestTranscription } from '../config/transcription.js';
import {
  getRecordings,
  getRecordingByRecordId,
  deleteRecording,
  publishRecording,
} from '../config/bbb.js';

const BBB_SECRET = process.env.BBB_SECRET || '';

const router = Router();

/**
 * Format a raw BBB recording into a normalised object.
 */
function formatBbbRecording(rec, fallbackName) {
  const playback = rec.playback?.format;
  let formats = [];
  if (playback) {
    formats = Array.isArray(playback) ? playback : [playback];
  }

  return {
    recordID: rec.recordID,
    meetingID: rec.meetingID,
    name: rec.name || fallbackName || 'Recording',
    state: rec.state,
    published: rec.published === 'true',
    startTime: rec.startTime,
    endTime: rec.endTime,
    participants: rec.participants,
    size: rec.size,
    formats: formats.map(f => ({
      type: f.type,
      url: f.url,
      length: f.length,
      size: f.size,
    })),
    metadata: rec.metadata || {},
  };
}

/**
 * Format a DB row into the same shape the frontend expects.
 */
function formatDbRecording(row) {
  return {
    recordID: row.record_id,
    meetingID: row.meeting_id,
    name: row.name || 'Recording',
    state: row.state,
    published: row.published === 1,
    startTime: row.start_time,
    endTime: row.end_time,
    participants: row.participants,
    size: row.size,
    formats: JSON.parse(row.formats || '[]'),
    metadata: JSON.parse(row.metadata || '{}'),
    fromCache: true,
  };
}

/**
 * Upsert a single formatted recording into the DB.
 * - If the record_id already exists AND is the same recording, merge new formats.
 * - If it's a new recording or a different one with the same record_id, overwrite.
 */
async function upsertRecording(db, rec) {
  const existing = await db.get('SELECT * FROM recordings WHERE record_id = ?', [rec.recordID]);
  const formatsJson = JSON.stringify(rec.formats);
  const metadataJson = JSON.stringify(rec.metadata || {});

  if (existing) {
    // Verify it's still the same recording (same startTime)
    if (String(rec.startTime) === String(existing.start_time)) {
      // Merge formats: keep existing formats, add any new types
      const existingFormats = JSON.parse(existing.formats || '[]');
      const existingTypes = new Set(existingFormats.map(f => f.type));
      const mergedFormats = [...existingFormats];

      for (const f of rec.formats) {
        if (existingTypes.has(f.type)) {
          // Update URL for existing format type (server may have changed)
          const idx = mergedFormats.findIndex(ef => ef.type === f.type);
          if (idx !== -1) mergedFormats[idx] = f;
        } else {
          // New format type added
          mergedFormats.push(f);
        }
      }

      await db.run(
        `UPDATE recordings SET name = ?, state = ?, published = ?, end_time = ?,
         participants = ?, size = ?, formats = ?, metadata = ?, updated_at = CURRENT_TIMESTAMP
         WHERE record_id = ?`,
        [rec.name, rec.state, rec.published ? 1 : 0, rec.endTime,
         rec.participants, rec.size, JSON.stringify(mergedFormats), metadataJson,
         rec.recordID]
      );
    } else {
      // Different recording with same record_id – overwrite completely
      await db.run(
        `UPDATE recordings SET meeting_id = ?, name = ?, state = ?, published = ?,
         start_time = ?, end_time = ?, participants = ?, size = ?, formats = ?,
         metadata = ?, updated_at = CURRENT_TIMESTAMP
         WHERE record_id = ?`,
        [rec.meetingID, rec.name, rec.state, rec.published ? 1 : 0,
         rec.startTime, rec.endTime, rec.participants, rec.size,
         formatsJson, metadataJson, rec.recordID]
      );
    }
  } else {
    // Completely new recording – insert
    await db.run(
      `INSERT INTO recordings (record_id, meeting_id, name, state, published, start_time,
       end_time, participants, size, formats, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [rec.recordID, rec.meetingID, rec.name, rec.state, rec.published ? 1 : 0,
       rec.startTime, rec.endTime, rec.participants, rec.size,
       formatsJson, metadataJson]
    );
  }
}

/**
 * Core logic: fetch recordings from BBB, sync with DB, fall back to DB on error.
 * Returns an array of formatted recordings.
 */
async function fetchAndSyncRecordings(meetingID, fallbackName) {
  const db = getDb();
  let bbbRecordings = null;

  try {
    const raw = await getRecordings(meetingID || undefined);
    bbbRecordings = raw.map(rec => formatBbbRecording(rec, fallbackName));
  } catch (err) {
    log.recordings.warn(`BBB API unreachable, falling back to cached recordings: ${err.message}`);
  }

  if (bbbRecordings !== null) {
    // BBB API was reachable – sync each recording into the DB
    for (const rec of bbbRecordings) {
      try {
        await upsertRecording(db, rec);
      } catch (err) {
        log.recordings.error(`Failed to cache recording ${rec.recordID}: ${err.message}`);
      }
    }

    // For recordings in DB that were NOT returned by BBB (possibly deleted on server
    // or server changed), keep them in DB with their cached URLs – don't delete.
    // However, we return the BBB result merged with any DB-only recordings.
    const bbbIds = new Set(bbbRecordings.map(r => r.recordID));
    const params = meetingID ? [meetingID] : [];
    const query = meetingID
      ? 'SELECT * FROM recordings WHERE meeting_id = ?'
      : 'SELECT * FROM recordings';
    const dbRows = await db.all(query, params);

    // Add DB-only recordings (not on current BBB server) with cached URLs
    for (const row of dbRows) {
      if (!bbbIds.has(row.record_id)) {
        bbbRecordings.push(formatDbRecording(row));
      }
    }

    return bbbRecordings;
  } else {
    // BBB API unreachable – serve everything from DB
    const params = meetingID ? [meetingID] : [];
    const query = meetingID
      ? 'SELECT * FROM recordings WHERE meeting_id = ?'
      : 'SELECT * FROM recordings';
    const rows = await db.all(query, params);
    return rows.map(formatDbRecording);
  }
}

// POST /api/recordings/recording-ready - BBB recording-ready callback (unauthenticated)
// BBB posts `signed_parameters`, a JWT signed with the shared BBB secret.
// Fires once per playback format; the transcription request is only sent when
// the "video" format first becomes available and the room has the setting on.
router.post('/recording-ready', urlencoded({ extended: false }), async (req, res) => {
  try {
    const signed = req.body?.signed_parameters;
    if (!signed || typeof signed !== 'string') {
      return res.status(400).json({ error: 'signed_parameters is required' });
    }

    let payload;
    try {
      payload = jwt.verify(signed, BBB_SECRET, { algorithms: ['HS256'] });
    } catch {
      return res.status(403).json({ error: 'Invalid signature' });
    }

    const recordId = payload.record_id;
    const meetingId = payload.meeting_id;
    if (!recordId || !meetingId) {
      return res.status(400).json({ error: 'record_id and meeting_id are required' });
    }

    // Always answer 200 from here on so BBB does not keep retrying —
    // "nothing to do" is a valid outcome for this callback.
    const db = getDb();
    const room = await db.get('SELECT id, uid, name, recording_transcript FROM rooms WHERE uid = ?', [meetingId]);
    if (!room || !room.recording_transcript || !isTranscriptionEnabled()) {
      return res.json({});
    }

    const rec = await getRecordingByRecordId(recordId).catch(() => null);
    if (!rec) return res.json({});

    const formatted = formatBbbRecording(rec, room.name);
    const hasVideo = formatted.formats.some(f => f.type === 'video');

    // Cache the recording so the dedupe marker below has a row to live on
    await upsertRecording(db, formatted);

    if (!hasVideo) return res.json({});

    // Dedupe: BBB fires the callback once per format
    const cached = await db.get('SELECT transcript_requested_at FROM recordings WHERE record_id = ?', [recordId]);
    if (cached?.transcript_requested_at) return res.json({});

    const ok = await requestTranscription({ meetingId, recordingId: recordId });
    if (ok) {
      await db.run('UPDATE recordings SET transcript_requested_at = CURRENT_TIMESTAMP WHERE record_id = ?', [recordId]);
    }

    res.json({});
  } catch (err) {
    log.recordings.error(`Recording-ready callback error: ${err.message}`);
    res.status(500).json({ error: 'Error processing recording-ready callback' });
  }
});

// GET /api/recordings - Get recordings for a room (by meetingID/uid)
router.get('/', authenticateToken, async (req, res) => {
  try {
    const { meetingID } = req.query;

    // M11: verify user has access to the room if a meetingID is specified
    if (meetingID) {
      const db = getDb();
      const room = await db.get('SELECT id, user_id FROM rooms WHERE uid = ?', [meetingID]);
      if (!room) {
        return res.status(404).json({ error: 'Room not found' });
      }
      if (room.user_id !== req.user.id && req.user.role !== 'admin') {
        const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
        if (!share) {
          return res.status(403).json({ error: 'No permission to view recordings for this room' });
        }
      }
    } else if (req.user.role !== 'admin') {
      // Non-admins must specify a meetingID
      return res.status(400).json({ error: 'meetingID query parameter is required' });
    }

    const formatted = await fetchAndSyncRecordings(meetingID);
    res.json({ recordings: formatted });
  } catch (err) {
    log.recordings.error(`Get recordings error: ${err.message}`);
    res.status(500).json({ error: 'Recordings could not be loaded', recordings: [] });
  }
});

// GET /api/recordings/room/:uid - Get recordings for a specific room
router.get('/room/:uid', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    const room = await db.get('SELECT * FROM rooms WHERE uid = ?', [req.params.uid]);

    if (!room) {
      return res.status(404).json({ error: 'Room not found' });
    }

    // H9: verify requesting user has access to this room
    if (room.user_id !== req.user.id && req.user.role !== 'admin') {
      const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
      if (!share) {
        return res.status(403).json({ error: 'No permission to view recordings for this room' });
      }
    }

    const formatted = await fetchAndSyncRecordings(room.uid, room.name);
    res.json({ recordings: formatted });
  } catch (err) {
    log.recordings.error(`Get room recordings error: ${err.message}`);
    res.status(500).json({ error: 'Recordings could not be loaded', recordings: [] });
  }
});

// DELETE /api/recordings/:recordID
router.delete('/:recordID', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    // M14 fix: look up the recording from BBB to find its meetingID (room UID),
    // then verify the user owns or shares that room.
    if (req.user.role !== 'admin') {
      // Try BBB first, fall back to DB cache
      let rec = await getRecordingByRecordId(req.params.recordID).catch(() => null);
      if (!rec) {
        const dbRow = await db.get('SELECT * FROM recordings WHERE record_id = ?', [req.params.recordID]);
        if (dbRow) {
          rec = { meetingID: dbRow.meeting_id };
        }
      }
      if (!rec) {
        return res.status(404).json({ error: 'Recording not found' });
      }
      const room = await db.get('SELECT id, user_id FROM rooms WHERE uid = ?', [rec.meetingID]);
      if (!room) {
        return res.status(404).json({ error: 'Room not found' });
      }
      if (room.user_id !== req.user.id) {
        const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
        if (!share) {
          return res.status(403).json({ error: 'No permission to delete this recording' });
        }
      }
    }
    // Try to delete on BBB (may fail if server changed – that's OK)
    try {
      await deleteRecording(req.params.recordID);
    } catch (err) {
      log.recordings.warn(`BBB deleteRecording failed (may already be gone): ${err.message}`);
    }
    // Always remove from local cache
    await db.run('DELETE FROM recordings WHERE record_id = ?', [req.params.recordID]);
    res.json({ message: 'Recording deleted' });
  } catch (err) {
    log.recordings.error(`Delete recording error: ${err.message}`);
    res.status(500).json({ error: 'Recording could not be deleted' });
  }
});

// PUT /api/recordings/:recordID/publish
router.put('/:recordID/publish', authenticateToken, async (req, res) => {
  try {
    const db = getDb();
    // M14 fix: look up the recording from BBB to find its meetingID (room UID),
    // then verify the user owns or shares that room.
    if (req.user.role !== 'admin') {
      let rec = await getRecordingByRecordId(req.params.recordID).catch(() => null);
      if (!rec) {
        const dbRow = await db.get('SELECT * FROM recordings WHERE record_id = ?', [req.params.recordID]);
        if (dbRow) {
          rec = { meetingID: dbRow.meeting_id };
        }
      }
      if (!rec) {
        return res.status(404).json({ error: 'Recording not found' });
      }
      const room = await db.get('SELECT id, user_id FROM rooms WHERE uid = ?', [rec.meetingID]);
      if (!room) {
        return res.status(404).json({ error: 'Room not found' });
      }
      if (room.user_id !== req.user.id) {
        const share = await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]);
        if (!share) {
          return res.status(403).json({ error: 'No permission to update this recording' });
        }
      }
    }
    const { publish } = req.body;
    // Try BBB API
    try {
      await publishRecording(req.params.recordID, publish);
    } catch (err) {
      log.recordings.warn(`BBB publishRecording failed: ${err.message}`);
    }
    // Update DB cache
    await db.run('UPDATE recordings SET published = ?, updated_at = CURRENT_TIMESTAMP WHERE record_id = ?',
      [publish ? 1 : 0, req.params.recordID]);
    res.json({ message: publish ? 'Recording published' : 'Recording unpublished' });
  } catch (err) {
    log.recordings.error(`Publish recording error: ${err.message}`);
    res.status(500).json({ error: 'Recording could not be updated' });
  }
});

export default router;
