import { BadRequestException } from '@nestjs/common';
import { randomUUID } from 'crypto';
import { PunchType, TripStatus } from '../common/constants/enums';
import type { TripDocument } from './schemas/trip.schema';
import type { AddNextClientDto } from './dto/add-next-client.dto';

export type TripLegRecord = Record<string, unknown>;

export function punchFromDto(
  dto: {
    timestamp: string;
    latitude: number;
    longitude: number;
    address?: string;
  },
  flutterType: string,
) {
  return {
    type: flutterType,
    time: dto.timestamp,
    latitude: dto.latitude,
    longitude: dto.longitude,
    address: dto.address ?? null,
  };
}

export function findActiveLegIndex(legs: TripLegRecord[]): number {
  const idx = legs.findIndex((leg) => !isLegComplete(leg));
  return idx === -1 ? Math.max(0, legs.length - 1) : idx;
}

function punchFromDb(punch: Record<string, unknown>, flutterType: string) {
  return {
    type: flutterType,
    time: punch.timestamp ?? null,
    latitude: punch.latitude ?? null,
    longitude: punch.longitude ?? null,
    address: punch.address ?? null,
  };
}

function mapDbPunchesToLeg(punches: Record<string, unknown>[]) {
  const byType = new Map<string, Record<string, unknown>>();
  for (const p of punches) {
    const t = String(p.type ?? '').toUpperCase();
    byType.set(t, p);
  }
  return {
    departurePunch: byType.has(PunchType.DEPARTURE)
      ? punchFromDb(byType.get(PunchType.DEPARTURE)!, 'travel_departure')
      : undefined,
    arrivalPunch: byType.has(PunchType.ARRIVAL)
      ? punchFromDb(byType.get(PunchType.ARRIVAL)!, 'travel_arrival')
      : undefined,
    meetingStartPunch: byType.has(PunchType.MEETING_START)
      ? punchFromDb(byType.get(PunchType.MEETING_START)!, 'meeting_start')
      : undefined,
    meetingEndPunch: byType.has(PunchType.MEETING_END)
      ? punchFromDb(byType.get(PunchType.MEETING_END)!, 'meeting_end')
      : undefined,
  };
}

function hydrateLegFromGlobalPunches(
  leg: TripLegRecord,
  global: ReturnType<typeof mapDbPunchesToLeg>,
): TripLegRecord {
  return {
    ...leg,
    departurePunch: hasPunch(leg, 'departurePunch')
      ? leg.departurePunch
      : global.departurePunch,
    arrivalPunch: hasPunch(leg, 'arrivalPunch')
      ? leg.arrivalPunch
      : global.arrivalPunch,
    meetingStartPunch: hasPunch(leg, 'meetingStartPunch')
      ? leg.meetingStartPunch
      : global.meetingStartPunch,
    meetingEndPunch: hasPunch(leg, 'meetingEndPunch')
      ? leg.meetingEndPunch
      : global.meetingEndPunch,
  };
}

export function hasPunch(leg: TripLegRecord, key: string): boolean {
  const punch = leg[key];
  return punch != null && typeof punch === 'object';
}

function isReturnLeg(leg: TripLegRecord): boolean {
  return leg.isReturnLeg === true;
}

export function isLegComplete(leg: TripLegRecord): boolean {
  if (!hasPunch(leg, 'departurePunch') || !hasPunch(leg, 'arrivalPunch')) {
    return false;
  }
  if (isReturnLeg(leg)) return true;
  return hasPunch(leg, 'meetingStartPunch') && hasPunch(leg, 'meetingEndPunch');
}

function punchTime(leg: TripLegRecord, key: string): Date | null {
  if (!hasPunch(leg, key)) return null;
  const punch = leg[key] as Record<string, unknown>;
  const raw = punch.time ?? punch.timestamp;
  if (!raw) return null;
  const parsed = new Date(String(raw));
  return Number.isNaN(parsed.getTime()) ? null : parsed;
}

/** Detects punches copied from an earlier leg onto a leg that has not truly started. */
export function legPunchesLookLeaked(
  leg: TripLegRecord,
  previous: TripLegRecord,
): boolean {
  const hasAny =
    hasPunch(leg, 'departurePunch') ||
    hasPunch(leg, 'arrivalPunch') ||
    hasPunch(leg, 'meetingStartPunch') ||
    hasPunch(leg, 'meetingEndPunch');
  if (!hasAny) return false;

  if (!isReturnLeg(previous) && !isLegComplete(previous)) return true;
  if (!hasPunch(leg, 'departurePunch')) return true;

  const departureTime = punchTime(leg, 'departurePunch');
  if (!departureTime) return true;

  const previousDeparture = punchTime(previous, 'departurePunch');
  if (
    previousDeparture &&
    departureTime.getTime() === previousDeparture.getTime()
  ) {
    return true;
  }

  const previousEndedAt =
    punchTime(previous, 'meetingEndPunch') ??
    punchTime(previous, 'arrivalPunch');
  if (
    previousEndedAt &&
    departureTime.getTime() <= previousEndedAt.getTime()
  ) {
    return true;
  }

  return false;
}

export function ensureTripLegs(
  trip: TripDocument,
  punches: Record<string, unknown>[] = [],
): TripLegRecord[] {
  const global = mapDbPunchesToLeg(punches);
  const existing = Array.isArray(trip.tripLegs)
    ? trip.tripLegs.map((leg) => ({ ...leg }))
    : [];

  if (existing.length > 0) {
    return existing.map((leg, index) => {
      if (index === 0 && !isReturnLeg(leg)) {
        return hydrateLegFromGlobalPunches(leg, global);
      }
      return leg;
    });
  }

  const from = trip.fromLocation?.trim() ?? '';
  const to = trip.toLocation?.trim() ?? '';
  if (!from && !to) return [];

  return [
    {
      legId: `${trip.tripId}_leg_1`,
      sequence: 1,
      fromLocation: from,
      toLocation: to,
      clientName: trip.clientName?.trim() ?? '',
      purpose: trip.purpose?.trim() ?? '',
      clientOfficeAddress: to,
      isReturnLeg: false,
      ...global,
    },
  ];
}

export function assertCanAddNextClient(legs: TripLegRecord[]) {
  if (legs.length === 0) {
    throw new BadRequestException('Trip has no legs');
  }
  const last = legs[legs.length - 1];
  if (isReturnLeg(last)) {
    throw new BadRequestException('Cannot add client after return leg started');
  }
  if (!isLegComplete(last)) {
    throw new BadRequestException(
      'Complete the current client meeting before adding next client',
    );
  }
}

export function addNextClientLeg(
  trip: TripDocument,
  dto: AddNextClientDto,
  punches: Record<string, unknown>[] = [],
): TripLegRecord[] {
  const toLocation = dto.toLocation?.trim();
  if (!toLocation) {
    throw new BadRequestException('toLocation is required');
  }

  const legs = ensureTripLegs(trip, punches);
  assertCanAddNextClient(legs);

  const previous = legs[legs.length - 1];
  const fromLocation =
    (previous.toLocation as string | undefined)?.trim() ?? '';

  const nextLeg: TripLegRecord = {
    legId: randomUUID(),
    sequence: legs.length + 1,
    fromLocation,
    toLocation,
    clientName: dto.clientName?.trim() ?? '',
    purpose: dto.purpose?.trim() ?? '',
    clientOfficeAddress: dto.clientOfficeAddress?.trim() || toLocation,
    isReturnLeg: false,
  };

  return [...legs, nextLeg];
}

export function assertCanStartReturnTrip(legs: TripLegRecord[]) {
  if (legs.length === 0) {
    throw new BadRequestException('Trip has no legs');
  }
  if (legs.some((leg) => isReturnLeg(leg))) {
    throw new BadRequestException('Return leg already exists');
  }
  const forwardLegs = legs.filter((leg) => !isReturnLeg(leg));
  const lastForward = forwardLegs[forwardLegs.length - 1];
  if (!lastForward || !isLegComplete(lastForward)) {
    throw new BadRequestException(
      'Complete all client visits before starting return trip',
    );
  }
}

export function addReturnLeg(
  trip: TripDocument,
  punches: Record<string, unknown>[] = [],
): TripLegRecord[] {
  const legs = ensureTripLegs(trip, punches);
  assertCanStartReturnTrip(legs);

  const first = legs[0];
  const previous = legs[legs.length - 1];
  const fromLocation =
    (previous.toLocation as string | undefined)?.trim() ?? '';
  const toLocation =
    (first.fromLocation as string | undefined)?.trim() ??
    trip.fromLocation?.trim() ??
    '';

  const returnLeg: TripLegRecord = {
    legId: randomUUID(),
    sequence: legs.length + 1,
    fromLocation,
    toLocation,
    clientName: '',
    purpose: 'Return to start',
    clientOfficeAddress: toLocation,
    isReturnLeg: true,
  };

  return [...legs, returnLeg];
}

export function syncTripTopLevelFromLegs(
  trip: TripDocument,
  legs: TripLegRecord[],
) {
  if (legs.length === 0) return;

  const activeIndex = legs.findIndex((leg) => !isLegComplete(leg));
  const currentIndex = activeIndex === -1 ? legs.length - 1 : activeIndex;
  const active = legs[currentIndex];
  const last = legs[legs.length - 1];
  const first = legs[0];

  trip.fromLocation =
    (first.fromLocation as string | undefined) ?? trip.fromLocation;
  trip.toLocation =
    (last.toLocation as string | undefined) ?? trip.toLocation;
  trip.clientName =
    (active.clientName as string | undefined)?.trim() ||
    (last.clientName as string | undefined)?.trim() ||
    trip.clientName;
  trip.purpose =
    (active.purpose as string | undefined)?.trim() ||
    (last.purpose as string | undefined)?.trim() ||
    trip.purpose;
  trip.currentLegIndex = currentIndex;
  trip.tripLegs = legs;

  if (activeIndex === -1) {
    trip.status = isReturnLeg(last)
      ? TripStatus.COMPLETED
      : TripStatus.MEETING_COMPLETED;
    return;
  }

  if (!isLegComplete(active)) {
    if (!hasPunch(active, 'departurePunch')) {
      trip.status = isReturnLeg(active)
        ? TripStatus.RETURN_TRIP
        : TripStatus.CREATED;
      return;
    }
    if (!hasPunch(active, 'arrivalPunch')) {
      trip.status = TripStatus.TRAVELLING;
      return;
    }
    if (!isReturnLeg(active) && !hasPunch(active, 'meetingStartPunch')) {
      trip.status = TripStatus.ARRIVED;
      return;
    }
    if (!isReturnLeg(active) && !hasPunch(active, 'meetingEndPunch')) {
      trip.status = TripStatus.MEETING_STARTED;
      return;
    }
  }

  if (isReturnLeg(last) && !isLegComplete(last)) {
    trip.status = TripStatus.RETURN_TRIP;
    return;
  }

  trip.status = TripStatus.MEETING_COMPLETED;
}
