import { randomBytes } from 'crypto';
import pool from '../config/database';
import logger from '../utils/logger';
import {
  Uebung,
  UebungWithAttendance,
  UebungListItem,
  MemberParticipationStats,
  CreateUebungData,
  UpdateUebungData,
  TeilnahmeStatus,
  Teilnahme,
} from '../models/training.model';

// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------

/** Columns used in all SELECT queries to hydrate a full Uebung row */
const UEBUNG_COLUMNS = `
  u.id, u.titel, u.beschreibung, u.typ::text AS typ,
  u.datum_von, u.datum_bis, u.ort, u.treffpunkt,
  u.pflichtveranstaltung, u.mindest_teilnehmer, u.max_teilnehmer,
  u.angelegt_von, u.erstellt_am, u.aktualisiert_am,
  u.abgesagt, u.absage_grund
`;

const ATTENDANCE_COUNT_COLUMNS = `
  COUNT(t.user_id)                                              AS gesamt_eingeladen,
  COUNT(t.user_id) FILTER (WHERE t.status = 'zugesagt')        AS anzahl_zugesagt,
  COUNT(t.user_id) FILTER (WHERE t.status = 'abgesagt')        AS anzahl_abgesagt,
  COUNT(t.user_id) FILTER (WHERE t.status = 'erschienen')      AS anzahl_erschienen,
  COUNT(t.user_id) FILTER (WHERE t.status = 'entschuldigt')    AS anzahl_entschuldigt,
  COUNT(t.user_id) FILTER (WHERE t.status = 'unbekannt')       AS anzahl_unbekannt
`;

/** Map a raw DB row to UebungListItem, optionally including eigener_status */
function rowToListItem(row: any, eigenerStatus?: TeilnahmeStatus): UebungListItem {
  return {
    id: row.id,
    titel: row.titel,
    typ: row.typ,
    datum_von: new Date(row.datum_von),
    datum_bis: new Date(row.datum_bis),
    ort: row.ort ?? null,
    pflichtveranstaltung: row.pflichtveranstaltung,
    abgesagt: row.abgesagt,
    anzahl_zugesagt: Number(row.anzahl_zugesagt ?? 0),
    anzahl_erschienen: Number(row.anzahl_erschienen ?? 0),
    gesamt_eingeladen: Number(row.gesamt_eingeladen ?? 0),
    eigener_status: eigenerStatus ?? row.eigener_status ?? undefined,
  };
}

function rowToUebung(row: any): Uebung {
  return {
    id: row.id,
    titel: row.titel,
    beschreibung: row.beschreibung ?? null,
    typ: row.typ,
    datum_von: new Date(row.datum_von),
    datum_bis: new Date(row.datum_bis),
    ort: row.ort ?? null,
    treffpunkt: row.treffpunkt ?? null,
    pflichtveranstaltung: row.pflichtveranstaltung,
    mindest_teilnehmer: row.mindest_teilnehmer ?? null,
    max_teilnehmer: row.max_teilnehmer ?? null,
    angelegt_von: row.angelegt_von ?? null,
    erstellt_am: new Date(row.erstellt_am),
    aktualisiert_am: new Date(row.aktualisiert_am),
    abgesagt: row.abgesagt,
    absage_grund: row.absage_grund ?? null,
  };
}

// ---------------------------------------------------------------------------
// Training Service
// ---------------------------------------------------------------------------

class TrainingService {
  /**
   * Returns upcoming (future) events sorted ascending by datum_von.
   * Used by the dashboard widget and the list view.
   */
  async getUpcomingEvents(limit = 10, userId?: string): Promise<UebungListItem[]> {
    const query = `
      SELECT
        ${UEBUNG_COLUMNS},
        ${ATTENDANCE_COUNT_COLUMNS}
        ${userId ? `, own_t.status::text AS eigener_status` : ''}
      FROM uebungen u
      LEFT JOIN uebung_teilnahmen t ON t.uebung_id = u.id
      ${userId ? `LEFT JOIN uebung_teilnahmen own_t ON own_t.uebung_id = u.id AND own_t.user_id = $2` : ''}
      WHERE u.datum_von > NOW()
        AND u.abgesagt = FALSE
      GROUP BY u.id ${userId ? `, own_t.status` : ''}
      ORDER BY u.datum_von ASC
      LIMIT $1
    `;

    const values = userId ? [limit, userId] : [limit];
    const result = await pool.query(query, values);
    return result.rows.map((r) => rowToListItem(r));
  }

  /**
   * Returns all events within a date range (inclusive) for the calendar view.
   * Does NOT filter out cancelled events — the frontend shows them struck through.
   */
  async getEventsByDateRange(from: Date, to: Date, userId?: string): Promise<UebungListItem[]> {
    const query = `
      SELECT
        ${UEBUNG_COLUMNS},
        ${ATTENDANCE_COUNT_COLUMNS}
        ${userId ? `, own_t.status::text AS eigener_status` : ''}
      FROM uebungen u
      LEFT JOIN uebung_teilnahmen t ON t.uebung_id = u.id
      ${userId ? `LEFT JOIN uebung_teilnahmen own_t ON own_t.uebung_id = u.id AND own_t.user_id = $3` : ''}
      WHERE (u.datum_von BETWEEN $1 AND $2 OR u.datum_bis BETWEEN $1 AND $2 OR (u.datum_von <= $1 AND u.datum_bis >= $2))
      GROUP BY u.id ${userId ? `, own_t.status` : ''}
      ORDER BY u.datum_von ASC
    `;

    const values = userId ? [from, to, userId] : [from, to];
    const result = await pool.query(query, values);
    return result.rows.map((r) => rowToListItem(r));
  }

  /**
   * Returns the full event detail including attendance counts and, for
   * privileged users, the individual attendee list.
   */
  async getEventById(
    id: string,
    userId?: string,
    includeTeilnahmen = false
  ): Promise<UebungWithAttendance | null> {
    const eventQuery = `
      SELECT
        ${UEBUNG_COLUMNS},
        ${ATTENDANCE_COUNT_COLUMNS},
        creator.name AS angelegt_von_name
        ${userId ? `, own_t.status::text AS eigener_status` : ''}
      FROM uebungen u
      LEFT JOIN uebung_teilnahmen t ON t.uebung_id = u.id
      LEFT JOIN users creator ON creator.id = u.angelegt_von
      ${userId ? `LEFT JOIN uebung_teilnahmen own_t ON own_t.uebung_id = u.id AND own_t.user_id = $2` : ''}
      WHERE u.id = $1
      GROUP BY u.id, creator.name ${userId ? `, own_t.status` : ''}
    `;

    const values = userId ? [id, userId] : [id];
    const eventResult = await pool.query(eventQuery, values);

    if (eventResult.rows.length === 0) return null;

    const row = eventResult.rows[0];
    const uebung = rowToUebung(row);

    const result: UebungWithAttendance = {
      ...uebung,
      gesamt_eingeladen: Number(row.gesamt_eingeladen ?? 0),
      anzahl_zugesagt: Number(row.anzahl_zugesagt ?? 0),
      anzahl_abgesagt: Number(row.anzahl_abgesagt ?? 0),
      anzahl_erschienen: Number(row.anzahl_erschienen ?? 0),
      anzahl_entschuldigt: Number(row.anzahl_entschuldigt ?? 0),
      anzahl_unbekannt: Number(row.anzahl_unbekannt ?? 0),
      angelegt_von_name: row.angelegt_von_name ?? null,
      eigener_status: row.eigener_status ?? undefined,
    };

    if (includeTeilnahmen) {
      const teilnahmenQuery = `
        SELECT
          t.uebung_id,
          t.user_id,
          t.status::text AS status,
          t.antwort_am,
          t.erschienen_erfasst_am,
          t.erschienen_erfasst_von,
          t.bemerkung,
          COALESCE(u.name, u.preferred_username, u.email) AS user_name,
          u.email AS user_email
        FROM uebung_teilnahmen t
        JOIN users u ON u.id = t.user_id
        WHERE t.uebung_id = $1
        ORDER BY u.name ASC NULLS LAST
      `;
      const teilnahmenResult = await pool.query(teilnahmenQuery, [id]);
      result.teilnahmen = teilnahmenResult.rows as Teilnahme[];
    }

    return result;
  }

  /**
   * Creates a new training event.
   * The database trigger automatically creates 'unbekannt' teilnahmen
   * rows for all active members.
   */
  async createEvent(data: CreateUebungData, createdBy: string): Promise<Uebung> {
    const query = `
      INSERT INTO uebungen (
        titel, beschreibung, typ, datum_von, datum_bis, ort, treffpunkt,
        pflichtveranstaltung, mindest_teilnehmer, max_teilnehmer, angelegt_von
      )
      VALUES ($1, $2, $3::uebung_typ, $4, $5, $6, $7, $8, $9, $10, $11)
      RETURNING
        id, titel, beschreibung, typ::text AS typ,
        datum_von, datum_bis, ort, treffpunkt,
        pflichtveranstaltung, mindest_teilnehmer, max_teilnehmer,
        angelegt_von, erstellt_am, aktualisiert_am,
        abgesagt, absage_grund
    `;

    const values = [
      data.titel,
      data.beschreibung ?? null,
      data.typ,
      data.datum_von,
      data.datum_bis,
      data.ort ?? null,
      data.treffpunkt ?? null,
      data.pflichtveranstaltung,
      data.mindest_teilnehmer ?? null,
      data.max_teilnehmer ?? null,
      createdBy,
    ];

    const result = await pool.query(query, values);
    const event = rowToUebung(result.rows[0]);

    logger.info('Training event created', {
      eventId: event.id,
      titel: event.titel,
      typ: event.typ,
      datum_von: event.datum_von,
      createdBy,
    });

    return event;
  }

  /**
   * Updates mutable fields of an existing event.
   * Only provided fields are updated (partial update semantics).
   */
  async updateEvent(
    id: string,
    data: UpdateUebungData,
    _updatedBy: string
  ): Promise<Uebung> {
    const fields: string[] = [];
    const values: unknown[] = [];
    let p = 1;

    const add = (col: string, val: unknown, cast = '') => {
      fields.push(`${col} = $${p++}${cast}`);
      values.push(val);
    };

    if (data.titel !== undefined)            add('titel', data.titel);
    if (data.beschreibung !== undefined)     add('beschreibung', data.beschreibung);
    if (data.typ !== undefined)              add('typ', data.typ, '::uebung_typ');
    if (data.datum_von !== undefined)        add('datum_von', data.datum_von);
    if (data.datum_bis !== undefined)        add('datum_bis', data.datum_bis);
    if (data.ort !== undefined)              add('ort', data.ort);
    if (data.treffpunkt !== undefined)       add('treffpunkt', data.treffpunkt);
    if (data.pflichtveranstaltung !== undefined) add('pflichtveranstaltung', data.pflichtveranstaltung);
    if (data.mindest_teilnehmer !== undefined)   add('mindest_teilnehmer', data.mindest_teilnehmer);
    if (data.max_teilnehmer !== undefined)       add('max_teilnehmer', data.max_teilnehmer);

    if (fields.length === 0) {
      // Nothing to update — return existing event
      const existing = await this.getEventById(id);
      if (!existing) throw new Error('Event not found');
      return existing;
    }

    values.push(id);
    const query = `
      UPDATE uebungen
      SET ${fields.join(', ')}
      WHERE id = $${p}
      RETURNING
        id, titel, beschreibung, typ::text AS typ,
        datum_von, datum_bis, ort, treffpunkt,
        pflichtveranstaltung, mindest_teilnehmer, max_teilnehmer,
        angelegt_von, erstellt_am, aktualisiert_am,
        abgesagt, absage_grund
    `;

    const result = await pool.query(query, values);
    if (result.rows.length === 0) throw new Error('Event not found');

    logger.info('Training event updated', { eventId: id, updatedBy: _updatedBy });
    return rowToUebung(result.rows[0]);
  }

  /**
   * Soft-cancels an event. Sets abgesagt=true and records the reason.
   * Does NOT delete the event or its attendance rows.
   */
  async cancelEvent(id: string, reason: string, updatedBy: string): Promise<void> {
    const result = await pool.query(
      `UPDATE uebungen
       SET abgesagt = TRUE, absage_grund = $2
       WHERE id = $1
       RETURNING id`,
      [id, reason]
    );

    if (result.rows.length === 0) throw new Error('Event not found');

    logger.info('Training event cancelled', { eventId: id, updatedBy, reason });
  }

  /**
   * Member updates their own RSVP — only 'zugesagt' or 'abgesagt' allowed here.
   * Sets antwort_am to now.
   */
  async updateAttendanceRSVP(
    uebungId: string,
    userId: string,
    status: 'zugesagt' | 'abgesagt',
    bemerkung?: string | null
  ): Promise<void> {
    const result = await pool.query(
      `UPDATE uebung_teilnahmen
       SET status       = $3::teilnahme_status,
           antwort_am   = NOW(),
           bemerkung    = COALESCE($4, bemerkung)
       WHERE uebung_id = $1 AND user_id = $2
       RETURNING uebung_id`,
      [uebungId, userId, status, bemerkung ?? null]
    );

    if (result.rows.length === 0) {
      // Row might not exist if member joined after event was created — insert it
      await pool.query(
        `INSERT INTO uebung_teilnahmen (uebung_id, user_id, status, antwort_am, bemerkung)
         VALUES ($1, $2, $3::teilnahme_status, NOW(), $4)
         ON CONFLICT (uebung_id, user_id) DO UPDATE
           SET status     = EXCLUDED.status,
               antwort_am = EXCLUDED.antwort_am,
               bemerkung  = COALESCE(EXCLUDED.bemerkung, uebung_teilnahmen.bemerkung)`,
        [uebungId, userId, status, bemerkung ?? null]
      );
    }

    logger.info('RSVP updated', { uebungId, userId, status });
  }

  /**
   * Gruppenführer / Kommandant bulk-marks members as 'erschienen'.
   * Marks erschienen_erfasst_am and erschienen_erfasst_von.
   */
  async markAttendance(
    uebungId: string,
    userIds: string[],
    markedBy: string
  ): Promise<void> {
    if (userIds.length === 0) return;

    // Build parameterized IN clause: $3, $4, $5, ...
    const placeholders = userIds.map((_, i) => `$${i + 3}`).join(', ');

    await pool.query(
      `UPDATE uebung_teilnahmen
       SET status                = 'erschienen'::teilnahme_status,
           erschienen_erfasst_am = NOW(),
           erschienen_erfasst_von = $2
       WHERE uebung_id = $1
         AND user_id IN (${placeholders})`,
      [uebungId, markedBy, ...userIds]
    );

    logger.info('Bulk attendance marked', {
      uebungId,
      count: userIds.length,
      markedBy,
    });
  }

  /**
   * Annual participation statistics for all active members.
   * Filters to events within the given calendar year.
   * "unbekannt" responses are NOT treated as absent.
   */
  async getMemberParticipationStats(year: number): Promise<MemberParticipationStats[]> {
    const query = `
      SELECT
        usr.id                                                              AS user_id,
        COALESCE(usr.name, usr.preferred_username, usr.email)              AS name,
        COUNT(t.uebung_id)                                                 AS total_uebungen,
        COUNT(t.uebung_id) FILTER (WHERE t.status = 'erschienen')          AS attended,
        COUNT(t.uebung_id) FILTER (WHERE u.pflichtveranstaltung = TRUE)    AS pflicht_gesamt,
        COUNT(t.uebung_id) FILTER (
            WHERE u.pflichtveranstaltung = TRUE AND t.status = 'erschienen'
        )                                                                  AS pflicht_erschienen,
        ROUND(
          CASE
            WHEN COUNT(t.uebung_id) FILTER (WHERE u.typ = 'Übungsabend') = 0 THEN 0
            ELSE
              COUNT(t.uebung_id) FILTER (
                WHERE u.typ = 'Übungsabend' AND t.status = 'erschienen'
              )::NUMERIC /
              COUNT(t.uebung_id) FILTER (WHERE u.typ = 'Übungsabend') * 100
          END, 1
        )                                                                  AS uebungsabend_quote_pct
      FROM users usr
      JOIN uebung_teilnahmen t ON t.user_id = usr.id
      JOIN uebungen u ON u.id = t.uebung_id
      WHERE usr.is_active = TRUE
        AND u.abgesagt = FALSE
        AND EXTRACT(YEAR FROM u.datum_von) = $1
      GROUP BY usr.id, usr.name, usr.preferred_username, usr.email
      ORDER BY name ASC
    `;

    const result = await pool.query(query, [year]);

    return result.rows.map((r) => ({
      userId: r.user_id,
      name: r.name,
      totalUebungen: Number(r.total_uebungen),
      attended: Number(r.attended),
      attendancePercent:
        Number(r.total_uebungen) === 0
          ? 0
          : Math.round((Number(r.attended) / Number(r.total_uebungen)) * 1000) / 10,
      pflichtGesamt: Number(r.pflicht_gesamt),
      pflichtErschienen: Number(r.pflicht_erschienen),
      uebungsabendQuotePct: Number(r.uebungsabend_quote_pct),
    }));
  }

  // ---------------------------------------------------------------------------
  // iCal token management
  // ---------------------------------------------------------------------------

  /**
   * Returns the existing calendar token for a user, or creates a new one.
   * Tokens are 32-byte hex strings (URL-safe).
   */
  async getOrCreateCalendarToken(userId: string): Promise<string> {
    const existing = await pool.query(
      `SELECT token FROM calendar_tokens WHERE user_id = $1`,
      [userId]
    );
    if (existing.rows.length > 0) return existing.rows[0].token;

    const token = randomBytes(32).toString('hex');
    await pool.query(
      `INSERT INTO calendar_tokens (user_id, token) VALUES ($1, $2)`,
      [userId, token]
    );
    return token;
  }

  /**
   * Looks up the userId associated with a calendar token and
   * touches last_used_at.
   */
  async resolveCalendarToken(token: string): Promise<string | null> {
    const result = await pool.query(
      `UPDATE calendar_tokens
       SET last_used_at = NOW()
       WHERE token = $1
       RETURNING user_id`,
      [token]
    );
    return result.rows[0]?.user_id ?? null;
  }

  /**
   * Generates iCal content for the given user (or public feed for all events
   * when userId is undefined).
   */
  async getCalendarExport(userId?: string): Promise<string> {
    // Fetch events for the next 12 months + past 3 months
    const from = new Date();
    from.setMonth(from.getMonth() - 3);
    const to = new Date();
    to.setFullYear(to.getFullYear() + 1);

    const events = await this.getEventsByDateRange(from, to, userId);
    return generateICS(events, 'Feuerwehr Rems');
  }
}

// ---------------------------------------------------------------------------
// iCal generation — zero-dependency RFC 5545 implementation
// ---------------------------------------------------------------------------

/**
 * Formats a Date to the iCal DTSTART/DTEND format with UTC timezone.
 * Output: 20260304T190000Z
 */
function formatIcsDate(date: Date): string {
  return date
    .toISOString()
    .replace(/[-:]/g, '')
    .replace(/\.\d{3}/, '');
}

/**
 * Folds long iCal lines at 75 octets (RFC 5545 §3.1).
 * Continuation lines start with a single space.
 */
function foldLine(line: string): string {
  if (Buffer.byteLength(line, 'utf-8') <= 75) return line;
  let folded = '';
  let currentLine = '';
  let currentBytes = 0;

  for (const char of line) {
    const charBytes = Buffer.byteLength(char, 'utf-8');
    if (currentBytes + charBytes > 75) {
      folded += currentLine + '\r\n ';
      currentLine = char;
      currentBytes = 1 + charBytes;
    } else {
      currentLine += char;
      currentBytes += charBytes;
    }
  }
  folded += currentLine;
  return folded;
}

/**
 * Escapes text field values per RFC 5545 §3.3.11.
 */
function escapeIcsText(value: string): string {
  return value
    .replace(/\\/g, '\\\\')
    .replace(/;/g, '\\;')
    .replace(/,/g, '\\,')
    .replace(/\n/g, '\\n')
    .replace(/\r/g, '');
}

/** Maps UebungTyp to a human-readable category string */
const TYP_CATEGORY: Record<string, string> = {
  'Übungsabend':    'Training',
  'Lehrgang':       'Course',
  'Sonderdienst':   'Special Duty',
  'Versammlung':    'Meeting',
  'Gemeinschaftsübung': 'Joint Exercise',
  'Sonstiges':      'Other',
};

export function generateICS(
  events: Array<{
    id: string;
    titel: string;
    beschreibung?: string | null;
    typ: string;
    datum_von: Date;
    datum_bis: Date;
    ort?: string | null;
    pflichtveranstaltung: boolean;
    abgesagt: boolean;
  }>,
  organizerName: string
): string {
  const lines: string[] = [
    'BEGIN:VCALENDAR',
    'VERSION:2.0',
    `PRODID:-//Feuerwehr Rems//Dashboard//DE`,
    'CALSCALE:GREGORIAN',
    'METHOD:PUBLISH',
    `X-WR-CALNAME:${escapeIcsText(organizerName)} - Dienstkalender`,
    'X-WR-TIMEZONE:Europe/Vienna',
    'X-WR-CALDESC:Übungs- und Dienstkalender der Feuerwehr Rems',
  ];

  const stampNow = formatIcsDate(new Date());

  for (const event of events) {
    const summary = event.abgesagt
      ? `[ABGESAGT] ${event.titel}`
      : event.pflichtveranstaltung
        ? `* ${event.titel}`
        : event.titel;

    const descParts: string[] = [];
    if (event.beschreibung) descParts.push(event.beschreibung);
    if (event.pflichtveranstaltung) descParts.push('PFLICHTVERANSTALTUNG');
    if (event.abgesagt) descParts.push('Diese Veranstaltung wurde abgesagt.');
    descParts.push(`Typ: ${event.typ}`);

    lines.push('BEGIN:VEVENT');
    lines.push(foldLine(`UID:${event.id}@feuerwehr-rems.at`));
    lines.push(`DTSTAMP:${stampNow}`);
    lines.push(`DTSTART:${formatIcsDate(event.datum_von)}`);
    lines.push(`DTEND:${formatIcsDate(event.datum_bis)}`);
    lines.push(foldLine(`SUMMARY:${escapeIcsText(summary)}`));

    if (descParts.length > 0) {
      lines.push(foldLine(`DESCRIPTION:${escapeIcsText(descParts.join('\n'))}`));
    }
    if (event.ort) {
      lines.push(foldLine(`LOCATION:${escapeIcsText(event.ort)}`));
    }

    lines.push(`CATEGORIES:${TYP_CATEGORY[event.typ] ?? 'Other'}`);

    if (event.abgesagt) {
      lines.push('STATUS:CANCELLED');
    }

    lines.push('END:VEVENT');
  }

  lines.push('END:VCALENDAR');
  return lines.join('\r\n') + '\r\n';
}

export default new TrainingService();
