import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../../common/prisma.service';
import { JourneyDirection } from '../seats/seats.dto';
import { checkDirectionConflict } from '../../common/utils/journey-direction.utils';

export interface Segment {
  fromStationId: string;
  toStationId: string;
  fromSequence: number;
  toSequence: number;
  fromName: string;
  toName: string;
}

@Injectable()
export class SegmentsService {
  constructor(private prisma: PrismaService) {}

  async getJourneySegments(
    scheduleId: string,
    originStationId: string,
    destinationStationId: string,
  ): Promise<Segment[]> {
    const stopTimes = await this.prisma.tripStopTime.findMany({
      where: { scheduleId },
      include: { station: true },
      orderBy: { sequence: 'asc' },
    });

    const originStop = stopTimes.find(st => st.stationId === originStationId);
    const destStop   = stopTimes.find(st => st.stationId === destinationStationId);

    if (!originStop || !destStop) {
      throw new BadRequestException('Origin or destination station not found on this schedule');
    }
    if (originStop.sequence >= destStop.sequence) {
      throw new BadRequestException('Origin must come before destination');
    }

    const segments: Segment[] = [];
    for (let i = originStop.sequence; i < destStop.sequence; i++) {
      const fromStop = stopTimes.find(st => st.sequence === i);
      const toStop   = stopTimes.find(st => st.sequence === i + 1);
      if (fromStop && toStop) {
        segments.push({
          fromStationId: fromStop.stationId,
          toStationId:   toStop.stationId,
          fromSequence:  fromStop.sequence,
          toSequence:    toStop.sequence,
          fromName:      fromStop.station.name,
          toName:        toStop.station.name,
        });
      }
    }
    return segments;
  }

  /**
   * Canonical per-seat availability check for a leg [reqFrom, reqTo) — the single
   * source of truth used by search results (availabilityByClass), the interactive
   * seatmap (SeatsService.resolveEffectiveStatuses), and hold-conflict checking, so
   * they can never disagree about whether a given seat is free. Previously
   * SeatsService maintained its own separately-written copy of this same
   * hold/booking-overlap logic, which could (and did) drift out of sync with this one.
   *
   * Overlap rule (strict):  existingFrom < reqTo  AND  reqFrom < existingTo
   *
   * This means two journeys that TOUCH at a boundary do NOT conflict:
   *   P1: A(1) → B(2)   reqFrom=1, reqTo=2
   *   P2: B(2) → D(4)   reqFrom=2, reqTo=4
   *   Check P1 vs P2: 1 < 4 AND 2 < 2  →  true AND false  →  NO conflict ✓
   *
   *   P3: A(1) → D(4)   reqFrom=1, reqTo=4
   *   Check P3 vs P2: 1 < 4 AND 2 < 4  →  true AND true   →  CONFLICT ✓
   *
   * journeyDirection lets a round-trip's OUTBOUND and RETURN holds coexist on the
   * same schedule without blocking each other (see checkDirectionConflict) — omit it
   * for one-way contexts, where it defaults to ONE_WAY (conflicts with anything).
   *
   * Sources checked:
   *   1. Active SeatHolds  — leg + direction decoded from createdBy JSON
   *      ({ originStationId, destinationStationId, journeyDirection })
   *   2. Active JourneySegments — per-leg rows for CONFIRMED / PENDING_PAYMENT journeys
   *      (JourneySegment carries no direction — a confirmed booking always blocks,
   *      regardless of the requester's own direction)
   *
   * Returns a map from seatId to 'HELD' | 'BOOKED' — seats with no entry are free.
   * BOOKED takes priority when a seat is somehow reported as both.
   */
  async getSeatAvailabilityMap(
    scheduleId: string,
    seatIds: string[],
    stopTimesForSeqLookup: ReadonlyArray<{ stationId: string; sequence: number }>,
    reqFrom: number,
    reqTo: number,
    journeyDirection: JourneyDirection = JourneyDirection.ONE_WAY,
  ): Promise<Map<string, 'HELD' | 'BOOKED'>> {
    const result = new Map<string, 'HELD' | 'BOOKED'>();
    if (seatIds.length === 0) return result;

    const seqOf = (stationId: string) =>
      stopTimesForSeqLookup.find(s => s.stationId === stationId)?.sequence;

    const seatIdSet = new Set(seatIds);
    const now = new Date();

    const [allHolds, bookedLegs] = await Promise.all([
      this.prisma.seatHold.findMany({
        where: { scheduleId, expiresAt: { gt: now } },
        select: { seatIds: true, createdBy: true },
      }),
      this.prisma.journeySegment.findMany({
        where: {
          scheduleId,
          seatId: { in: seatIds },
          journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
        },
        select: { seatId: true, journeyId: true, departureStationId: true, arrivalStationId: true },
      }),
    ]);

    // ── 1. Active holds ────────────────────────────────────────────────────────
    for (const hold of allHolds) {
      let holdFrom: number | undefined;
      let holdTo: number | undefined;
      let holdDirection = JourneyDirection.ONE_WAY;
      try {
        if (hold.createdBy) {
          const meta = JSON.parse(hold.createdBy as string);
          holdFrom = seqOf(meta.originStationId);
          holdTo = seqOf(meta.destinationStationId);
          holdDirection = meta.journeyDirection || JourneyDirection.ONE_WAY;
        }
      } catch { /* ignore */ }

      for (const sid of hold.seatIds) {
        if (!seatIdSet.has(sid)) continue;
        // Conservative block if leg can't be resolved; otherwise check overlap.
        const legsOverlap = holdFrom === undefined || holdTo === undefined || (holdFrom < reqTo && reqFrom < holdTo);
        if (!legsOverlap) continue;
        if (!checkDirectionConflict(journeyDirection, holdDirection)) continue;
        result.set(sid, 'HELD');
      }
    }

    // ── 2. Active JourneySegments — per-seat, per-journey leg ranges ──────────
    const journeyRangesBySeat = new Map<string, Map<string, { from: number; to: number }>>();
    for (const leg of bookedLegs) {
      if (!leg.seatId || !leg.journeyId || !leg.departureStationId || !leg.arrivalStationId) continue;
      const depSeq = seqOf(leg.departureStationId);
      const arrSeq = seqOf(leg.arrivalStationId);
      if (depSeq === undefined || arrSeq === undefined) continue;

      let rangeMap = journeyRangesBySeat.get(leg.seatId);
      if (!rangeMap) { rangeMap = new Map(); journeyRangesBySeat.set(leg.seatId, rangeMap); }

      const existing = rangeMap.get(leg.journeyId);
      rangeMap.set(leg.journeyId, existing
        ? { from: Math.min(existing.from, depSeq), to: Math.max(existing.to, arrSeq) }
        : { from: depSeq, to: arrSeq });
    }

    for (const seatId of seatIds) {
      const rangeMap = journeyRangesBySeat.get(seatId);
      if (!rangeMap) continue;
      for (const { from, to } of rangeMap.values()) {
        if (from < reqTo && reqFrom < to) { result.set(seatId, 'BOOKED'); break; }
      }
    }

    return result;
  }

  /**
   * Batch availability check for multiple seats on a single schedule.
   * Thin wrapper around getSeatAvailabilityMap — returns just the free-seat set.
   */
  async getFreeSeatIds(
    scheduleId: string,
    seatIds: string[],
    stopTimesForSeqLookup: ReadonlyArray<{ stationId: string; sequence: number }>,
    reqFrom: number,
    reqTo: number,
    journeyDirection: JourneyDirection = JourneyDirection.ONE_WAY,
  ): Promise<Set<string>> {
    if (seatIds.length === 0) return new Set();
    const statusMap = await this.getSeatAvailabilityMap(
      scheduleId, seatIds, stopTimesForSeqLookup, reqFrom, reqTo, journeyDirection,
    );
    return new Set(seatIds.filter(id => !statusMap.has(id)));
  }

  /**
   * Single-seat convenience wrapper around getSeatAvailabilityMap.
   */
  async isSeatFreeForLeg(
    scheduleId: string,
    seatId: string,
    reqFrom: number,
    reqTo: number,
    journeyDirection: JourneyDirection = JourneyDirection.ONE_WAY,
  ): Promise<boolean> {
    const stopTimes = await this.prisma.tripStopTime.findMany({
      where: { scheduleId },
      select: { stationId: true, sequence: true },
    });
    const freeSeats = await this.getFreeSeatIds(scheduleId, [seatId], stopTimes, reqFrom, reqTo, journeyDirection);
    return freeSeats.has(seatId);
  }

  /** Legacy wrapper used by EnhancedSeatsService.getOverlappingReservations */
  async getOverlappingReservations(
    scheduleId: string,
    seatId: string,
    requestedSegments: Segment[],
  ): Promise<{ type: string; id: string }[]> {
    const reqFrom = Math.min(...requestedSegments.map(s => s.fromSequence));
    const reqTo   = Math.max(...requestedSegments.map(s => s.toSequence));
    const free = await this.isSeatFreeForLeg(scheduleId, seatId, reqFrom, reqTo);
    return free ? [] : [{ type: 'conflict', id: seatId }];
  }

  segmentsOverlap(segments1: Segment[], segments2: Segment[]): boolean {
    for (const s1 of segments1) {
      for (const s2 of segments2) {
        if (s1.fromSequence < s2.toSequence && s2.fromSequence < s1.toSequence) return true;
      }
    }
    return false;
  }
}
