import { createHash } from 'crypto';
import { TrackingEventType } from '../common/constants/enums';

export interface CoveragePoint {
  timestamp: Date;
  latitude: number;
  longitude: number;
  legId?: string;
}

export interface CoverageTrackingEvent {
  type: TrackingEventType | string;
  timestamp: Date;
  legId?: string;
  metadata?: Record<string, unknown>;
}

export interface CoverageGap {
  from: Date;
  to: Date;
  durationMinutes: number;
  reason: string;
  suspectedCause?: string;
}

export interface LegCoverageInput {
  legId: string;
  legNumber: number;
  fromLocation?: string;
  toLocation?: string;
  departureAt: Date;
  arrivalAt: Date | null;
  points: CoveragePoint[];
  events?: CoverageTrackingEvent[];
}

export interface LegCoverageResult {
  legId: string;
  legNumber: number;
  fromLocation?: string;
  toLocation?: string;
  departureAt: string;
  arrivalAt: string | null;
  expectedDurationMinutes: number;
  trackedDurationMinutes: number;
  gapDurationMinutes: number;
  coveragePercent: number;
  pointCount: number;
  gaps: Array<{
    from: string;
    to: string;
    durationMinutes: number;
    reason: string;
    suspectedCause?: string;
  }>;
}

const KILL_TYPES = new Set([
  TrackingEventType.OS_KILL_SUSPECTED,
  TrackingEventType.TRACKING_STOPPED,
  'os_kill_suspected',
  'tracking_stopped',
]);

const PERMISSION_TYPES = new Set([
  TrackingEventType.PERMISSION_DENIED,
  'permission_denied',
]);

const NETWORK_TYPES = new Set([
  TrackingEventType.NETWORK_OFFLINE,
  'network_offline',
]);

export function pointDedupeKey(
  timestamp: Date,
  latitude: number,
  longitude: number,
): string {
  const ts = timestamp.getTime();
  return createHash('sha1')
    .update(`${ts}:${latitude.toFixed(6)}:${longitude.toFixed(6)}`)
    .digest('hex');
}

export function dedupeCoveragePoints(points: CoveragePoint[]): CoveragePoint[] {
  const seen = new Set<string>();
  const out: CoveragePoint[] = [];
  for (const p of [...points].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())) {
    const key = pointDedupeKey(p.timestamp, p.latitude, p.longitude);
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(p);
  }
  return out;
}

function roundMinutes(ms: number): number {
  return Math.round((ms / 60000) * 10) / 10;
}

function clampDate(d: Date, min: Date, max: Date): Date {
  const t = d.getTime();
  return new Date(Math.min(max.getTime(), Math.max(min.getTime(), t)));
}

function overlaps(aStart: Date, aEnd: Date, bStart: Date, bEnd: Date): boolean {
  return aStart.getTime() < bEnd.getTime() && bStart.getTime() < aEnd.getTime();
}

function inferSuspectedCause(
  gapFrom: Date,
  gapTo: Date,
  events: CoverageTrackingEvent[],
): string | undefined {
  const inGap = events.filter((e) =>
    overlaps(gapFrom, gapTo, e.timestamp, e.timestamp),
  );
  if (!inGap.length) return undefined;

  for (const e of inGap) {
    if (PERMISSION_TYPES.has(e.type as TrackingEventType)) {
      return 'permission_denied';
    }
  }
  for (const e of inGap) {
    if (KILL_TYPES.has(e.type as TrackingEventType)) {
      return 'app_killed';
    }
  }
  for (const e of inGap) {
    if (NETWORK_TYPES.has(e.type as TrackingEventType)) {
      return 'network_offline';
    }
  }
  return undefined;
}

export function computeLegCoverage(
  input: LegCoverageInput,
  gapThresholdSeconds: number,
  now: Date = new Date(),
): LegCoverageResult {
  const windowStart = input.departureAt;
  const windowEnd = input.arrivalAt ?? now;
  const startMs = windowStart.getTime();
  const endMs = Math.max(startMs, windowEnd.getTime());
  const expectedMs = endMs - startMs;

  const legPoints = dedupeCoveragePoints(
    input.points.filter((p) => {
      const t = p.timestamp.getTime();
      if (t < startMs || t > endMs) return false;
      if (input.legId && p.legId && p.legId !== input.legId) return false;
      return true;
    }),
  );

  const thresholdMs = gapThresholdSeconds * 1000;
  const rawGaps: Array<{ from: Date; to: Date }> = [];

  if (!legPoints.length) {
    if (expectedMs > thresholdMs) {
      rawGaps.push({ from: windowStart, to: windowEnd });
    }
  } else {
    const first = legPoints[0].timestamp;
    if (first.getTime() - startMs > thresholdMs) {
      rawGaps.push({ from: windowStart, to: first });
    }

    for (let i = 0; i < legPoints.length - 1; i++) {
      const cur = legPoints[i].timestamp;
      const next = legPoints[i + 1].timestamp;
      const delta = next.getTime() - cur.getTime();
      if (delta > thresholdMs) {
        rawGaps.push({ from: cur, to: next });
      }
    }

    const last = legPoints[legPoints.length - 1].timestamp;
    if (endMs - last.getTime() > thresholdMs) {
      rawGaps.push({ from: last, to: windowEnd });
    }
  }

  const events = input.events ?? [];
  const gaps: CoverageGap[] = rawGaps.map((g) => {
    const from = clampDate(g.from, windowStart, windowEnd);
    const to = clampDate(g.to, windowStart, windowEnd);
    const durationMs = Math.max(0, to.getTime() - from.getTime());
    return {
      from,
      to,
      durationMinutes: roundMinutes(durationMs),
      reason: 'no_points',
      suspectedCause: inferSuspectedCause(from, to, events),
    };
  });

  const gapMs = gaps.reduce(
    (sum, g) => sum + (g.to.getTime() - g.from.getTime()),
    0,
  );
  const trackedMs = Math.max(0, expectedMs - gapMs);
  const coveragePercent =
    expectedMs > 0
      ? Math.round((trackedMs / expectedMs) * 1000) / 10
      : legPoints.length > 0
        ? 100
        : 0;

  return {
    legId: input.legId,
    legNumber: input.legNumber,
    fromLocation: input.fromLocation,
    toLocation: input.toLocation,
    departureAt: windowStart.toISOString(),
    arrivalAt: input.arrivalAt?.toISOString() ?? null,
    expectedDurationMinutes: roundMinutes(expectedMs),
    trackedDurationMinutes: roundMinutes(trackedMs),
    gapDurationMinutes: roundMinutes(gapMs),
    coveragePercent,
    pointCount: legPoints.length,
    gaps: gaps.map((g) => ({
      from: g.from.toISOString(),
      to: g.to.toISOString(),
      durationMinutes: g.durationMinutes,
      reason: g.reason,
      suspectedCause: g.suspectedCause,
    })),
  };
}

export function aggregateCoverageSummary(legs: LegCoverageResult[]) {
  const expectedMs = legs.reduce(
    (s, l) => s + l.expectedDurationMinutes * 60000,
    0,
  );
  const trackedMs = legs.reduce(
    (s, l) => s + l.trackedDurationMinutes * 60000,
    0,
  );
  const gapMs = legs.reduce((s, l) => s + l.gapDurationMinutes * 60000, 0);
  return {
    expectedDurationMinutes: roundMinutes(expectedMs),
    trackedDurationMinutes: roundMinutes(trackedMs),
    gapDurationMinutes: roundMinutes(gapMs),
    coveragePercent:
      expectedMs > 0
        ? Math.round((trackedMs / expectedMs) * 1000) / 10
        : 0,
  };
}
