import pool from '../config/database';
import logger from '../utils/logger';
import {
  FahrzeugBuchung,
  FahrzeugBuchungListItem,
  CreateBuchungData,
  UpdateBuchungData,
} from '../models/booking.model';

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

/** Format a Date to iCal YYYYMMDDTHHMMSSZ format (UTC) */
function toIcalDate(d: Date): string {
  return new Date(d).toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
}

function rowToListItem(row: any): FahrzeugBuchungListItem {
  return {
    id: row.id,
    fahrzeug_id: row.fahrzeug_id,
    fahrzeug_name: row.fahrzeug_name ?? '',
    fahrzeug_kennzeichen: row.fahrzeug_kennzeichen ?? null,
    titel: row.titel,
    buchungs_art: row.buchungs_art,
    beginn: new Date(row.beginn),
    ende: new Date(row.ende),
    ganztaegig: row.ganztaegig ?? false,
    abgesagt: row.abgesagt,
    gebucht_von: row.gebucht_von,
    gebucht_von_name: row.gebucht_von_name ?? null,
  };
}

function rowToBuchung(row: any): FahrzeugBuchung {
  return {
    id: row.id,
    fahrzeug_id: row.fahrzeug_id,
    titel: row.titel,
    beschreibung: row.beschreibung ?? null,
    beginn: new Date(row.beginn),
    ende: new Date(row.ende),
    buchungs_art: row.buchungs_art,
    ganztaegig: row.ganztaegig ?? false,
    gebucht_von: row.gebucht_von,
    kontakt_person: row.kontakt_person ?? null,
    kontakt_telefon: row.kontakt_telefon ?? null,
    abgesagt: row.abgesagt,
    abgesagt_grund: row.abgesagt_grund ?? null,
    erstellt_am: new Date(row.erstellt_am),
    aktualisiert_am: new Date(row.aktualisiert_am),
    fahrzeug_name: row.fahrzeug_name ?? null,
    fahrzeug_kennzeichen: row.fahrzeug_kennzeichen ?? null,
    gebucht_von_name: row.gebucht_von_name ?? null,
  };
}

// ---------------------------------------------------------------------------
// Booking Service
// ---------------------------------------------------------------------------

class BookingService {
  /**
   * Returns bookings overlapping the given date range, optionally filtered
   * to a single vehicle. Non-cancelled only.
   */
  async getBookingsByRange(
    from: Date,
    to: Date,
    fahrzeugId?: string
  ): Promise<FahrzeugBuchungListItem[]> {
    const params: unknown[] = [from, to];
    let vehicleFilter = '';
    if (fahrzeugId) {
      params.push(fahrzeugId);
      vehicleFilter = `AND b.fahrzeug_id = $${params.length}`;
    }

    const query = `
      SELECT
        b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
        b.beginn, b.ende, b.abgesagt, b.gebucht_von, b.ganztaegig,
        f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
        u.name AS gebucht_von_name
      FROM fahrzeug_buchungen b
      JOIN fahrzeuge f ON f.id = b.fahrzeug_id
      JOIN users u ON u.id = b.gebucht_von
      WHERE b.abgesagt = FALSE
        AND (
          b.beginn BETWEEN $1 AND $2
          OR b.ende BETWEEN $1 AND $2
          OR (b.beginn <= $1 AND b.ende >= $2)
        )
        ${vehicleFilter}
      ORDER BY b.beginn ASC
    `;

    const { rows } = await pool.query(query, params);
    return rows.map(rowToListItem);
  }

  /** Returns the next N upcoming non-cancelled bookings sorted ascending. */
  async getUpcoming(limit = 20): Promise<FahrzeugBuchungListItem[]> {
    const query = `
      SELECT
        b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art,
        b.beginn, b.ende, b.abgesagt, b.gebucht_von, b.ganztaegig,
        f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
        u.name AS gebucht_von_name
      FROM fahrzeug_buchungen b
      JOIN fahrzeuge f ON f.id = b.fahrzeug_id
      JOIN users u ON u.id = b.gebucht_von
      WHERE b.abgesagt = FALSE
        AND b.beginn > NOW()
      ORDER BY b.beginn ASC
      LIMIT $1
    `;

    const { rows } = await pool.query(query, [limit]);
    return rows.map(rowToListItem);
  }

  /** Returns a single booking by ID including all joined fields, or null. */
  async getById(id: string): Promise<FahrzeugBuchung | null> {
    const query = `
      SELECT
        b.id, b.fahrzeug_id, b.titel, b.beschreibung,
        b.buchungs_art::text AS buchungs_art,
        b.beginn, b.ende, b.ganztaegig,
        b.gebucht_von, b.kontakt_person, b.kontakt_telefon,
        b.abgesagt, b.abgesagt_grund,
        b.erstellt_am, b.aktualisiert_am,
        f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen,
        u.name AS gebucht_von_name
      FROM fahrzeug_buchungen b
      JOIN fahrzeuge f ON f.id = b.fahrzeug_id
      JOIN users u ON u.id = b.gebucht_von
      WHERE b.id = $1
    `;

    const { rows } = await pool.query(query, [id]);
    if (rows.length === 0) return null;
    return rowToBuchung(rows[0]);
  }

  /**
   * Checks whether a vehicle is out of service during the given interval.
   * Returns the out-of-service period if there IS a conflict, null otherwise.
   */
  async checkOutOfServiceConflict(
    fahrzeugId: string,
    beginn: Date,
    ende: Date
  ): Promise<{ ausser_dienst_von: Date; ausser_dienst_bis: Date } | null> {
    const query = `
      SELECT ausser_dienst_von, ausser_dienst_bis
      FROM fahrzeuge
      WHERE id = $1
        AND deleted_at IS NULL
        AND status IN ('ausser_dienst_wartung', 'ausser_dienst_schaden')
        AND ausser_dienst_von IS NOT NULL
        AND ausser_dienst_bis IS NOT NULL
        AND ($2::timestamptz, $3::timestamptz) OVERLAPS (ausser_dienst_von, ausser_dienst_bis)
      LIMIT 1
    `;

    const { rows } = await pool.query(query, [fahrzeugId, beginn, ende]);
    if (rows.length === 0) return null;
    return {
      ausser_dienst_von: new Date(rows[0].ausser_dienst_von),
      ausser_dienst_bis: new Date(rows[0].ausser_dienst_bis),
    };
  }

  /**
   * Checks whether a vehicle is already booked for the given interval.
   * Returns true if there IS a conflict.
   * Pass excludeId to ignore a specific booking (used during updates).
   */
  async checkConflict(
    fahrzeugId: string,
    beginn: Date,
    ende: Date,
    excludeId?: string
  ): Promise<boolean> {
    const query = `
      SELECT 1
      FROM fahrzeug_buchungen
      WHERE fahrzeug_id = $1
        AND abgesagt = FALSE
        AND ($2::timestamptz, $3::timestamptz) OVERLAPS (beginn, ende)
        AND ($4::uuid IS NULL OR id != $4)
      LIMIT 1
    `;

    const { rows } = await pool.query(query, [
      fahrzeugId,
      beginn,
      ende,
      excludeId ?? null,
    ]);
    return rows.length > 0;
  }

  /** Creates a new booking. Throws if the vehicle has a conflicting booking or is out of service (unless overridden). */
  async create(data: CreateBuchungData, userId: string, ignoreOutOfService = false): Promise<FahrzeugBuchung> {
    if (!ignoreOutOfService) {
      const outOfService = await this.checkOutOfServiceConflict(data.fahrzeugId, data.beginn, data.ende);
      if (outOfService) {
        throw new Error('Fahrzeug ist im gewählten Zeitraum außer Dienst');
      }
    }

    const hasConflict = await this.checkConflict(
      data.fahrzeugId,
      data.beginn,
      data.ende
    );
    if (hasConflict) {
      throw new Error('Fahrzeug ist im gewählten Zeitraum bereits gebucht');
    }

    const query = `
      INSERT INTO fahrzeug_buchungen
        (fahrzeug_id, titel, beschreibung, beginn, ende, buchungs_art, gebucht_von, kontakt_person, kontakt_telefon, ganztaegig)
      VALUES
        ($1, $2, $3, $4, $5, $6::fahrzeug_buchung_art, $7, $8, $9, $10)
      RETURNING id
    `;

    const { rows } = await pool.query(query, [
      data.fahrzeugId,
      data.titel,
      data.beschreibung ?? null,
      data.beginn,
      data.ende,
      data.buchungsArt,
      userId,
      data.kontaktPerson ?? null,
      data.kontaktTelefon ?? null,
      data.ganztaegig ?? false,
    ]);

    const newId: string = rows[0].id;
    const booking = await this.getById(newId);
    if (!booking) throw new Error('Buchung konnte nach dem Erstellen nicht geladen werden');
    return booking;
  }

  /**
   * Updates the provided fields of a booking.
   * Checks for conflicts when timing or vehicle fields change.
   */
  async update(id: string, data: UpdateBuchungData): Promise<FahrzeugBuchung | null> {
    const existing = await this.getById(id);
    if (!existing) return null;

    // Resolve effective values for conflict check
    const effectiveFahrzeugId = data.fahrzeugId ?? existing.fahrzeug_id;
    const effectiveBeginn = data.beginn ?? existing.beginn;
    const effectiveEnde = data.ende ?? existing.ende;

    const timingChanged =
      data.fahrzeugId != null || data.beginn != null || data.ende != null;

    if (timingChanged) {
      const outOfService = await this.checkOutOfServiceConflict(
        effectiveFahrzeugId,
        effectiveBeginn,
        effectiveEnde
      );
      if (outOfService) {
        throw new Error('Fahrzeug ist im gewählten Zeitraum außer Dienst');
      }

      const hasConflict = await this.checkConflict(
        effectiveFahrzeugId,
        effectiveBeginn,
        effectiveEnde,
        id
      );
      if (hasConflict) {
        throw new Error('Fahrzeug ist im gewählten Zeitraum bereits gebucht');
      }
    }

    // Build dynamic SET clause
    const setClauses: string[] = [];
    const params: unknown[] = [];

    const addField = (column: string, value: unknown, cast?: string) => {
      params.push(value);
      setClauses.push(`${column} = $${params.length}${cast ? `::${cast}` : ''}`);
    };

    if (data.fahrzeugId !== undefined) addField('fahrzeug_id', data.fahrzeugId);
    if (data.titel !== undefined) addField('titel', data.titel);
    if (data.beschreibung !== undefined) addField('beschreibung', data.beschreibung);
    if (data.beginn !== undefined) addField('beginn', data.beginn);
    if (data.ende !== undefined) addField('ende', data.ende);
    if (data.buchungsArt !== undefined) addField('buchungs_art', data.buchungsArt, 'fahrzeug_buchung_art');
    if (data.kontaktPerson !== undefined) addField('kontakt_person', data.kontaktPerson);
    if (data.kontaktTelefon !== undefined) addField('kontakt_telefon', data.kontaktTelefon);
    if (data.ganztaegig !== undefined) addField('ganztaegig', data.ganztaegig);

    if (setClauses.length === 0) {
      throw new Error('No fields to update');
    }

    params.push(id);
    const query = `
      UPDATE fahrzeug_buchungen
      SET ${setClauses.join(', ')}, aktualisiert_am = NOW()
      WHERE id = $${params.length}
    `;

    await pool.query(query, params);
    return this.getById(id);
  }

  /** Soft-cancels a booking by setting abgesagt=TRUE and recording the reason. */
  async cancel(id: string, abgesagt_grund: string): Promise<void> {
    const result = await pool.query(
      `UPDATE fahrzeug_buchungen
       SET abgesagt = TRUE, abgesagt_grund = $2, aktualisiert_am = NOW()
       WHERE id = $1`,
      [id, abgesagt_grund]
    );
    if (result.rowCount === 0) throw new Error('Buchung nicht gefunden');
  }

  /** Permanently deletes a booking record. */
  async delete(id: string): Promise<void> {
    const result = await pool.query('DELETE FROM fahrzeug_buchungen WHERE id = $1', [id]);
    if (result.rowCount === 0) throw new Error('Buchung nicht gefunden');
  }

  /**
   * Returns an existing iCal token for the user, or creates a new one.
   * Also returns the subscribe URL the user can add to their calendar app.
   */
  async getOrCreateIcalToken(
    userId: string
  ): Promise<{ token: string; subscribeUrl: string }> {
    const selectResult = await pool.query(
      'SELECT token FROM fahrzeug_ical_tokens WHERE user_id = $1',
      [userId]
    );

    let token: string;
    if (selectResult.rows.length > 0) {
      token = selectResult.rows[0].token;
    } else {
      const insertResult = await pool.query(
        `INSERT INTO fahrzeug_ical_tokens (user_id)
         VALUES ($1)
         RETURNING token`,
        [userId]
      );
      token = insertResult.rows[0].token;
      logger.info('Created new iCal token for user', { userId });
    }

    const baseUrl = (process.env.ICAL_BASE_URL || process.env.CORS_ORIGIN || 'http://localhost:3000').replace(/\/$/, '');
    const subscribeUrl = `${baseUrl}/api/bookings/calendar.ics?token=${token}`;
    return { token, subscribeUrl };
  }

  /**
   * Validates the iCal token and returns an iCal string for all (or one
   * vehicle's) non-cancelled bookings. Returns null when the token is invalid.
   */
  async getIcalExport(
    token: string,
    fahrzeugId?: string
  ): Promise<string | null> {
    // Validate token and update last-used timestamp
    const tokenResult = await pool.query(
      `UPDATE fahrzeug_ical_tokens
       SET zuletzt_verwendet_am = NOW()
       WHERE token = $1
       RETURNING id`,
      [token]
    );
    if (tokenResult.rows.length === 0) return null;

    // Fetch bookings
    const params: unknown[] = [];
    let vehicleFilter = '';
    if (fahrzeugId) {
      params.push(fahrzeugId);
      vehicleFilter = `AND b.fahrzeug_id = $${params.length}`;
    }

    const query = `
      SELECT
        b.id, b.titel, b.beschreibung, b.buchungs_art::text AS buchungs_art,
        b.beginn, b.ende,
        f.bezeichnung AS fahrzeug_name
      FROM fahrzeug_buchungen b
      JOIN fahrzeuge f ON f.id = b.fahrzeug_id
      WHERE b.abgesagt = FALSE
        ${vehicleFilter}
      ORDER BY b.beginn ASC
    `;

    const { rows } = await pool.query(query, params);
    const now = toIcalDate(new Date());

    // iCal escaping and folding helpers
    const icalEscape = (val: string): string =>
      val.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
    const icalFold = (line: string): string => {
      if (Buffer.byteLength(line, 'utf-8') <= 75) return line;
      let folded = '';
      let cur = '';
      let bytes = 0;
      for (const ch of line) {
        const cb = Buffer.byteLength(ch, 'utf-8');
        if (bytes + cb > 75) { folded += cur + '\r\n '; cur = ch; bytes = 1 + cb; }
        else { cur += ch; bytes += cb; }
      }
      return folded + cur;
    };

    const events = rows
      .map((row: any) => {
        const beschreibung = [row.buchungs_art, row.beschreibung]
          .filter(Boolean)
          .join(' | ');
        return (
          'BEGIN:VEVENT\r\n' +
          `UID:${row.id}@feuerwehr-buchungen\r\n` +
          `DTSTAMP:${now}\r\n` +
          `DTSTART:${toIcalDate(new Date(row.beginn))}\r\n` +
          `DTEND:${toIcalDate(new Date(row.ende))}\r\n` +
          icalFold(`SUMMARY:${icalEscape(row.titel)} - ${icalEscape(row.fahrzeug_name)}`) + '\r\n' +
          icalFold(`DESCRIPTION:${icalEscape(beschreibung)}`) + '\r\n' +
          'END:VEVENT\r\n'
        );
      })
      .join('');

    return (
      'BEGIN:VCALENDAR\r\n' +
      'VERSION:2.0\r\n' +
      'PRODID:-//Feuerwehr Dashboard//Fahrzeugbuchungen//DE\r\n' +
      'X-WR-CALNAME:Feuerwehr Fahrzeugbuchungen\r\n' +
      'X-WR-TIMEZONE:Europe/Berlin\r\n' +
      'CALSCALE:GREGORIAN\r\n' +
      events +
      'END:VCALENDAR\r\n'
    );
  }
}

export default new BookingService();
