import pool from '../config/database';
import logger from '../utils/logger';
import {
  Einsatz,
  EinsatzWithDetails,
  EinsatzListItem,
  EinsatzStats,
  EinsatzFahrzeug,
  EinsatzPersonal,
  MonthlyStatRow,
  EinsatzArtStatRow,
  EinsatzArt,
  CreateEinsatzData,
  UpdateEinsatzData,
  AssignPersonnelData,
  AssignVehicleData,
  IncidentFilters,
} from '../models/incident.model';

class IncidentService {
  // -------------------------------------------------------------------------
  // LIST
  // -------------------------------------------------------------------------

  /**
   * Get a paginated list of incidents with optional filters.
   * Returns lightweight EinsatzListItem rows (no bericht_text, no sub-arrays).
   */
  async getAllIncidents(
    filters: IncidentFilters = { limit: 50, offset: 0 }
  ): Promise<{ items: EinsatzListItem[]; total: number }> {
    try {
      const conditions: string[] = ["e.status != 'archiviert'"];
      const params: unknown[] = [];
      let p = 1;

      if (filters.dateFrom) {
        conditions.push(`e.alarm_time >= $${p++}`);
        params.push(filters.dateFrom);
      }
      if (filters.dateTo) {
        conditions.push(`e.alarm_time <= $${p++}`);
        params.push(filters.dateTo);
      }
      if (filters.einsatzArt) {
        conditions.push(`e.einsatz_art = $${p++}`);
        params.push(filters.einsatzArt);
      }
      if (filters.status) {
        conditions.push(`e.status = $${p++}`);
        params.push(filters.status);
      }

      const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

      // Count query
      const countResult = await pool.query(
        `SELECT COUNT(*)::INTEGER AS total FROM einsaetze e ${where}`,
        params
      );
      const total: number = countResult.rows[0].total;

      // Data query — joined with users for einsatzleiter name and personal count
      const dataQuery = `
        SELECT
          e.id,
          e.einsatz_nr,
          e.alarm_time,
          e.einsatz_art,
          e.einsatz_stichwort,
          e.ort,
          e.strasse,
          e.status,
          COALESCE(
            TRIM(CONCAT(u.given_name, ' ', u.family_name)),
            u.name,
            u.preferred_username
          )                          AS einsatzleiter_name,
          ROUND(
            EXTRACT(EPOCH FROM (e.ankunft_time - e.alarm_time)) / 60.0
          )::INTEGER                 AS hilfsfrist_min,
          ROUND(
            EXTRACT(EPOCH FROM (e.einrueck_time - e.alarm_time)) / 60.0
          )::INTEGER                 AS dauer_min,
          COALESCE(ep.personal_count, 0)::INTEGER AS personal_count
        FROM einsaetze e
        LEFT JOIN users u ON u.id = e.einsatzleiter_id
        LEFT JOIN (
          SELECT einsatz_id, COUNT(*) AS personal_count
          FROM einsatz_personal
          GROUP BY einsatz_id
        ) ep ON ep.einsatz_id = e.id
        ${where}
        ORDER BY e.alarm_time DESC
        LIMIT $${p++} OFFSET $${p++}
      `;

      params.push(filters.limit ?? 50, filters.offset ?? 0);
      const dataResult = await pool.query(dataQuery, params);

      const items: EinsatzListItem[] = dataResult.rows.map((row) => ({
        id: row.id,
        einsatz_nr: row.einsatz_nr,
        alarm_time: row.alarm_time,
        einsatz_art: row.einsatz_art,
        einsatz_stichwort: row.einsatz_stichwort,
        ort: row.ort,
        strasse: row.strasse,
        status: row.status,
        einsatzleiter_name: row.einsatzleiter_name ?? null,
        hilfsfrist_min: row.hilfsfrist_min !== null ? Number(row.hilfsfrist_min) : null,
        dauer_min: row.dauer_min !== null ? Number(row.dauer_min) : null,
        personal_count: Number(row.personal_count),
      }));

      return { items, total };
    } catch (error) {
      logger.error('Error fetching incident list', { error, filters });
      throw new Error('Failed to fetch incidents');
    }
  }

  // -------------------------------------------------------------------------
  // DETAIL
  // -------------------------------------------------------------------------

  /**
   * Get a single incident with full details: personnel, vehicles, computed times.
   * NOTE: bericht_text is included here; role-based redaction is applied in
   * the controller based on req.user.role.
   */
  async getIncidentById(id: string): Promise<EinsatzWithDetails | null> {
    try {
      const einsatzResult = await pool.query(
        `
        SELECT
          e.*,
          COALESCE(
            TRIM(CONCAT(u.given_name, ' ', u.family_name)),
            u.name,
            u.preferred_username
          ) AS einsatzleiter_name,
          ROUND(
            EXTRACT(EPOCH FROM (e.ankunft_time - e.alarm_time)) / 60.0
          )::INTEGER AS hilfsfrist_min,
          ROUND(
            EXTRACT(EPOCH FROM (e.einrueck_time - e.alarm_time)) / 60.0
          )::INTEGER AS dauer_min
        FROM einsaetze e
        LEFT JOIN users u ON u.id = e.einsatzleiter_id
        WHERE e.id = $1
        `,
        [id]
      );

      if (einsatzResult.rows.length === 0) {
        return null;
      }

      const row = einsatzResult.rows[0];

      // Fetch assigned personnel
      const personalResult = await pool.query(
        `
        SELECT
          ep.einsatz_id,
          ep.user_id,
          ep.funktion,
          ep.alarm_time,
          ep.ankunft_time,
          ep.assigned_at,
          u.name,
          u.email,
          u.given_name,
          u.family_name
        FROM einsatz_personal ep
        JOIN users u ON u.id = ep.user_id
        WHERE ep.einsatz_id = $1
        ORDER BY ep.funktion, u.family_name
        `,
        [id]
      );

      // Fetch assigned vehicles
      const fahrzeugeResult = await pool.query(
        `
        SELECT
          ef.einsatz_id,
          ef.fahrzeug_id,
          ef.ausrueck_time,
          ef.einrueck_time,
          ef.assigned_at,
          f.amtliches_kennzeichen AS kennzeichen,
          f.bezeichnung,
          f.typ_schluessel        AS fahrzeug_typ
        FROM einsatz_fahrzeuge ef
        JOIN fahrzeuge f ON f.id = ef.fahrzeug_id
        WHERE ef.einsatz_id = $1
        ORDER BY f.bezeichnung
        `,
        [id]
      );

      const einsatz: EinsatzWithDetails = {
        id: row.id,
        einsatz_nr: row.einsatz_nr,
        alarm_time: row.alarm_time,
        ausrueck_time: row.ausrueck_time,
        ankunft_time: row.ankunft_time,
        einrueck_time: row.einrueck_time,
        einsatz_art: row.einsatz_art,
        einsatz_stichwort: row.einsatz_stichwort,
        strasse: row.strasse,
        hausnummer: row.hausnummer,
        ort: row.ort,
        koordinaten: row.koordinaten,
        bericht_kurz: row.bericht_kurz,
        bericht_text: row.bericht_text,
        einsatzleiter_id: row.einsatzleiter_id,
        alarmierung_art: row.alarmierung_art,
        status: row.status,
        created_by: row.created_by,
        created_at: row.created_at,
        updated_at: row.updated_at,
        einsatzleiter_name: row.einsatzleiter_name ?? null,
        hilfsfrist_min: row.hilfsfrist_min !== null ? Number(row.hilfsfrist_min) : null,
        dauer_min: row.dauer_min !== null ? Number(row.dauer_min) : null,
        fahrzeuge: fahrzeugeResult.rows as EinsatzFahrzeug[],
        personal: personalResult.rows as EinsatzPersonal[],
      };

      return einsatz;
    } catch (error) {
      logger.error('Error fetching incident by ID', { error, id });
      throw new Error('Failed to fetch incident');
    }
  }

  // -------------------------------------------------------------------------
  // CREATE
  // -------------------------------------------------------------------------

  /**
   * Create a new incident.
   * Einsatz-Nr is generated atomically by the PostgreSQL function
   * generate_einsatz_nr() using a per-year sequence table with UPDATE ... RETURNING.
   * This is safe under concurrent inserts.
   */
  async createIncident(data: CreateEinsatzData, createdBy: string): Promise<Einsatz> {
    const client = await pool.connect();
    try {
      await client.query('BEGIN');

      // Generate the Einsatz-Nr atomically inside the transaction
      const nrResult = await client.query(
        `SELECT generate_einsatz_nr($1::TIMESTAMPTZ) AS einsatz_nr`,
        [data.alarm_time]
      );
      const einsatz_nr: string = nrResult.rows[0].einsatz_nr;

      const result = await client.query(
        `
        INSERT INTO einsaetze (
          einsatz_nr, alarm_time, ausrueck_time, ankunft_time, einrueck_time,
          einsatz_art, einsatz_stichwort,
          strasse, hausnummer, ort,
          bericht_kurz, bericht_text,
          einsatzleiter_id, alarmierung_art, status,
          created_by
        ) VALUES (
          $1, $2, $3, $4, $5,
          $6, $7,
          $8, $9, $10,
          $11, $12,
          $13, $14, $15,
          $16
        )
        RETURNING *
        `,
        [
          einsatz_nr,
          data.alarm_time,
          data.ausrueck_time ?? null,
          data.ankunft_time ?? null,
          data.einrueck_time ?? null,
          data.einsatz_art,
          data.einsatz_stichwort ?? null,
          data.strasse ?? null,
          data.hausnummer ?? null,
          data.ort ?? null,
          data.bericht_kurz ?? null,
          data.bericht_text ?? null,
          data.einsatzleiter_id ?? null,
          data.alarmierung_art ?? 'ILS',
          data.status ?? 'aktiv',
          createdBy,
        ]
      );

      await client.query('COMMIT');

      const einsatz = result.rows[0] as Einsatz;
      logger.info('Incident created', {
        einsatzId: einsatz.id,
        einsatz_nr: einsatz.einsatz_nr,
        createdBy,
      });

      return einsatz;
    } catch (error) {
      await client.query('ROLLBACK');
      logger.error('Error creating incident', { error, createdBy });
      throw new Error('Failed to create incident');
    } finally {
      client.release();
    }
  }

  // -------------------------------------------------------------------------
  // UPDATE
  // -------------------------------------------------------------------------

  async updateIncident(
    id: string,
    data: UpdateEinsatzData,
    updatedBy: string
  ): Promise<Einsatz> {
    try {
      const fields: string[] = [];
      const values: unknown[] = [];
      let p = 1;

      const fieldMap: Array<[keyof UpdateEinsatzData, string]> = [
        ['alarm_time', 'alarm_time'],
        ['ausrueck_time', 'ausrueck_time'],
        ['ankunft_time', 'ankunft_time'],
        ['einrueck_time', 'einrueck_time'],
        ['einsatz_art', 'einsatz_art'],
        ['einsatz_stichwort', 'einsatz_stichwort'],
        ['strasse', 'strasse'],
        ['hausnummer', 'hausnummer'],
        ['ort', 'ort'],
        ['bericht_kurz', 'bericht_kurz'],
        ['bericht_text', 'bericht_text'],
        ['einsatzleiter_id', 'einsatzleiter_id'],
        ['alarmierung_art', 'alarmierung_art'],
        ['status', 'status'],
      ];

      for (const [key, col] of fieldMap) {
        if (key in data) {
          fields.push(`${col} = $${p++}`);
          values.push((data as Record<string, unknown>)[key] ?? null);
        }
      }

      if (fields.length === 0) {
        // Nothing to update — return current state
        const current = await this.getIncidentById(id);
        if (!current) throw new Error('Incident not found');
        return current as Einsatz;
      }

      // updated_at is handled by the trigger, but we also set it explicitly
      // to ensure immediate consistency within the same request cycle
      fields.push(`updated_at = NOW()`);
      values.push(id);

      const result = await pool.query(
        `
        UPDATE einsaetze
        SET ${fields.join(', ')}
        WHERE id = $${p}
        RETURNING *
        `,
        values
      );

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

      const einsatz = result.rows[0] as Einsatz;
      logger.info('Incident updated', {
        einsatzId: einsatz.id,
        einsatz_nr: einsatz.einsatz_nr,
        updatedBy,
      });

      return einsatz;
    } catch (error) {
      logger.error('Error updating incident', { error, id, updatedBy });
      if (error instanceof Error && error.message === 'Incident not found') throw error;
      throw new Error('Failed to update incident');
    }
  }

  // -------------------------------------------------------------------------
  // SOFT DELETE
  // -------------------------------------------------------------------------

  /** Soft delete: sets status = 'archiviert'. Hard delete is not exposed. */
  async deleteIncident(id: string, deletedBy: string): Promise<void> {
    try {
      const result = await pool.query(
        `
        UPDATE einsaetze
        SET status = 'archiviert', updated_at = NOW()
        WHERE id = $1 AND status != 'archiviert'
        RETURNING id
        `,
        [id]
      );

      if (result.rows.length === 0) {
        throw new Error('Incident not found or already archived');
      }

      logger.info('Incident archived (soft-deleted)', { einsatzId: id, deletedBy });
    } catch (error) {
      logger.error('Error archiving incident', { error, id });
      if (error instanceof Error && error.message.includes('not found')) throw error;
      throw new Error('Failed to archive incident');
    }
  }

  // -------------------------------------------------------------------------
  // PERSONNEL
  // -------------------------------------------------------------------------

  async assignPersonnel(einsatzId: string, data: AssignPersonnelData): Promise<void> {
    try {
      await pool.query(
        `
        INSERT INTO einsatz_personal (einsatz_id, user_id, funktion, alarm_time, ankunft_time)
        VALUES ($1, $2, $3, $4, $5)
        ON CONFLICT (einsatz_id, user_id) DO UPDATE SET
          funktion     = EXCLUDED.funktion,
          alarm_time   = EXCLUDED.alarm_time,
          ankunft_time = EXCLUDED.ankunft_time
        `,
        [
          einsatzId,
          data.user_id,
          data.funktion ?? 'Mannschaft',
          data.alarm_time ?? null,
          data.ankunft_time ?? null,
        ]
      );

      logger.info('Personnel assigned to incident', {
        einsatzId,
        userId: data.user_id,
        funktion: data.funktion,
      });
    } catch (error) {
      logger.error('Error assigning personnel', { error, einsatzId, data });
      throw new Error('Failed to assign personnel');
    }
  }

  async removePersonnel(einsatzId: string, userId: string): Promise<void> {
    try {
      const result = await pool.query(
        `DELETE FROM einsatz_personal WHERE einsatz_id = $1 AND user_id = $2 RETURNING user_id`,
        [einsatzId, userId]
      );

      if (result.rows.length === 0) {
        throw new Error('Personnel assignment not found');
      }

      logger.info('Personnel removed from incident', { einsatzId, userId });
    } catch (error) {
      logger.error('Error removing personnel', { error, einsatzId, userId });
      if (error instanceof Error && error.message.includes('not found')) throw error;
      throw new Error('Failed to remove personnel');
    }
  }

  // -------------------------------------------------------------------------
  // VEHICLES
  // -------------------------------------------------------------------------

  async assignVehicle(einsatzId: string, data: AssignVehicleData): Promise<void> {
    try {
      await pool.query(
        `
        INSERT INTO einsatz_fahrzeuge (einsatz_id, fahrzeug_id, ausrueck_time, einrueck_time)
        VALUES ($1, $2, $3, $4)
        ON CONFLICT (einsatz_id, fahrzeug_id) DO UPDATE SET
          ausrueck_time = EXCLUDED.ausrueck_time,
          einrueck_time = EXCLUDED.einrueck_time
        `,
        [
          einsatzId,
          data.fahrzeug_id,
          data.ausrueck_time ?? null,
          data.einrueck_time ?? null,
        ]
      );

      logger.info('Vehicle assigned to incident', { einsatzId, fahrzeugId: data.fahrzeug_id });
    } catch (error) {
      logger.error('Error assigning vehicle', { error, einsatzId, data });
      throw new Error('Failed to assign vehicle');
    }
  }

  async removeVehicle(einsatzId: string, fahrzeugId: string): Promise<void> {
    try {
      const result = await pool.query(
        `DELETE FROM einsatz_fahrzeuge WHERE einsatz_id = $1 AND fahrzeug_id = $2 RETURNING fahrzeug_id`,
        [einsatzId, fahrzeugId]
      );

      if (result.rows.length === 0) {
        throw new Error('Vehicle assignment not found');
      }

      logger.info('Vehicle removed from incident', { einsatzId, fahrzeugId });
    } catch (error) {
      logger.error('Error removing vehicle', { error, einsatzId, fahrzeugId });
      if (error instanceof Error && error.message.includes('not found')) throw error;
      throw new Error('Failed to remove vehicle');
    }
  }

  // -------------------------------------------------------------------------
  // STATISTICS
  // -------------------------------------------------------------------------

  /**
   * Returns aggregated statistics for a given year (defaults to current year).
   * Queries live data directly rather than the materialized view so the stats
   * page always reflects uncommitted-or-just-committed incidents.
   * The materialized view is used for dashboard KPI cards via a separate endpoint.
   */
  async getIncidentStats(year?: number): Promise<EinsatzStats> {
    try {
      const targetYear = year ?? new Date().getFullYear();
      const prevYear = targetYear - 1;

      // Overall totals for target year
      const totalsResult = await pool.query(
        `
        SELECT
          COUNT(*)::INTEGER                                          AS gesamt,
          COUNT(*) FILTER (WHERE status = 'abgeschlossen')::INTEGER AS abgeschlossen,
          COUNT(*) FILTER (WHERE status = 'aktiv')::INTEGER         AS aktiv,
          ROUND(
            AVG(
              EXTRACT(EPOCH FROM (ankunft_time - alarm_time)) / 60.0
            ) FILTER (WHERE ankunft_time IS NOT NULL)
          )::INTEGER AS avg_hilfsfrist_min
        FROM einsaetze
        WHERE EXTRACT(YEAR FROM alarm_time) = $1
          AND status != 'archiviert'
        `,
        [targetYear]
      );

      const totals = totalsResult.rows[0] ?? { gesamt: 0, abgeschlossen: 0, aktiv: 0, avg_hilfsfrist_min: null };

      // Monthly breakdown — target year
      const monthlyResult = await pool.query(
        `
        SELECT
          EXTRACT(MONTH FROM alarm_time)::INTEGER AS monat,
          COUNT(*)::INTEGER AS anzahl,
          ROUND(
            AVG(
              EXTRACT(EPOCH FROM (ankunft_time - alarm_time)) / 60.0
            ) FILTER (WHERE ankunft_time IS NOT NULL)
          )::INTEGER AS avg_hilfsfrist_min,
          ROUND(
            AVG(
              EXTRACT(EPOCH FROM (einrueck_time - alarm_time)) / 60.0
            ) FILTER (WHERE einrueck_time IS NOT NULL)
          )::INTEGER AS avg_dauer_min
        FROM einsaetze
        WHERE EXTRACT(YEAR FROM alarm_time) = $1
          AND status != 'archiviert'
        GROUP BY EXTRACT(MONTH FROM alarm_time)
        ORDER BY monat
        `,
        [targetYear]
      );

      // Monthly breakdown — previous year (for chart overlay)
      const prevMonthlyResult = await pool.query(
        `
        SELECT
          EXTRACT(MONTH FROM alarm_time)::INTEGER AS monat,
          COUNT(*)::INTEGER AS anzahl,
          ROUND(
            AVG(
              EXTRACT(EPOCH FROM (ankunft_time - alarm_time)) / 60.0
            ) FILTER (WHERE ankunft_time IS NOT NULL)
          )::INTEGER AS avg_hilfsfrist_min,
          ROUND(
            AVG(
              EXTRACT(EPOCH FROM (einrueck_time - alarm_time)) / 60.0
            ) FILTER (WHERE einrueck_time IS NOT NULL)
          )::INTEGER AS avg_dauer_min
        FROM einsaetze
        WHERE EXTRACT(YEAR FROM alarm_time) = $1
          AND status != 'archiviert'
        GROUP BY EXTRACT(MONTH FROM alarm_time)
        ORDER BY monat
        `,
        [prevYear]
      );

      // By Einsatzart — target year
      const byArtResult = await pool.query(
        `
        SELECT
          einsatz_art,
          COUNT(*)::INTEGER AS anzahl,
          ROUND(
            AVG(
              EXTRACT(EPOCH FROM (ankunft_time - alarm_time)) / 60.0
            ) FILTER (WHERE ankunft_time IS NOT NULL)
          )::INTEGER AS avg_hilfsfrist_min
        FROM einsaetze
        WHERE EXTRACT(YEAR FROM alarm_time) = $1
          AND status != 'archiviert'
        GROUP BY einsatz_art
        ORDER BY anzahl DESC
        `,
        [targetYear]
      );

      // Determine most common Einsatzart
      const haeufigste_art: EinsatzArt | null =
        byArtResult.rows.length > 0 ? (byArtResult.rows[0].einsatz_art as EinsatzArt) : null;

      const monthly: MonthlyStatRow[] = (monthlyResult.rows ?? []).map((r) => ({
        monat: r.monat,
        anzahl: r.anzahl,
        avg_hilfsfrist_min: r.avg_hilfsfrist_min !== null ? Number(r.avg_hilfsfrist_min) : null,
        avg_dauer_min: r.avg_dauer_min !== null ? Number(r.avg_dauer_min) : null,
      }));

      const prev_year_monthly: MonthlyStatRow[] = (prevMonthlyResult.rows ?? []).map((r) => ({
        monat: r.monat,
        anzahl: r.anzahl,
        avg_hilfsfrist_min: r.avg_hilfsfrist_min !== null ? Number(r.avg_hilfsfrist_min) : null,
        avg_dauer_min: r.avg_dauer_min !== null ? Number(r.avg_dauer_min) : null,
      }));

      const by_art: EinsatzArtStatRow[] = (byArtResult.rows ?? []).map((r) => ({
        einsatz_art: r.einsatz_art as EinsatzArt,
        anzahl: r.anzahl,
        avg_hilfsfrist_min: r.avg_hilfsfrist_min !== null ? Number(r.avg_hilfsfrist_min) : null,
      }));

      return {
        jahr: targetYear,
        gesamt: totals.gesamt ?? 0,
        abgeschlossen: totals.abgeschlossen ?? 0,
        aktiv: totals.aktiv ?? 0,
        avg_hilfsfrist_min:
          totals.avg_hilfsfrist_min !== null ? Number(totals.avg_hilfsfrist_min) : null,
        haeufigste_art,
        monthly,
        by_art,
        prev_year_monthly,
      };
    } catch (error) {
      logger.error('Error fetching incident statistics', { error, year });
      throw new Error('Failed to fetch incident statistics');
    }
  }

  // -------------------------------------------------------------------------
  // MATERIALIZED VIEW REFRESH
  // -------------------------------------------------------------------------

  /** Refresh the einsatz_statistik materialized view. Call after bulk operations. */
  async refreshStatistikView(): Promise<void> {
    try {
      await pool.query('REFRESH MATERIALIZED VIEW CONCURRENTLY einsatz_statistik');
      logger.info('einsatz_statistik materialized view refreshed');
    } catch (error) {
      // CONCURRENTLY requires a unique index — fall back to non-concurrent refresh
      try {
        await pool.query('REFRESH MATERIALIZED VIEW einsatz_statistik');
        logger.info('einsatz_statistik materialized view refreshed (non-concurrent)');
      } catch (fallbackError) {
        logger.error('Error refreshing einsatz_statistik view', { fallbackError });
        throw new Error('Failed to refresh statistics view');
      }
    }
  }
}

export default new IncidentService();
