type TimelinePhoto = {
  id: string;
  capturedAt: string | null;
};

type TimelineViewport = {
  start: number;
  end: number;
} | null;

type TimelineBin = {
  start: Date;
  end: Date;
  count: number;
  photoIds: string[];
  label: string;
};

export function getPhotoTimestamp(photo: TimelinePhoto): Date | null {
  if (!photo.capturedAt) {
    return null;
  }

  const date = new Date(photo.capturedAt);
  return Number.isNaN(date.getTime()) ? null : date;
}

function getTimelineIntervalMs(spanMs: number): number {
  const minute = 60 * 1000;
  const hour = 60 * minute;

  if (spanMs <= 12 * hour) {
    return 5 * minute;
  }

  if (spanMs <= 2 * 24 * hour) {
    return 15 * minute;
  }

  if (spanMs <= 7 * 24 * hour) {
    return 30 * minute;
  }

  if (spanMs <= 21 * 24 * hour) {
    return 60 * minute;
  }

  if (spanMs <= 60 * 24 * hour) {
    return 2 * hour;
  }

  return 4 * hour;
}

function floorToInterval(date: Date, intervalMs: number): Date {
  return new Date(Math.floor(date.getTime() / intervalMs) * intervalMs);
}

function ceilToInterval(date: Date, intervalMs: number): Date {
  return new Date(Math.ceil(date.getTime() / intervalMs) * intervalMs);
}

function formatTimelineLabel(date: Date): string {
  return date.toLocaleString("en-US", {
    month: "short",
    day: "numeric",
    hour: "2-digit",
    minute: "2-digit"
  });
}

export function formatTimelineSummary(photos: TimelinePhoto[]): string {
  if (!photos.length) {
    return "No photos imported yet.";
  }

  const count = photos.length + (photos.length === 1 ? " photo" : " photos");
  return count + " across a compact timeline.";
}

export function buildTimeline(photos: TimelinePhoto[], viewport: TimelineViewport = null) {
  const datedPhotos = photos
    .map((photo) => ({ photo, date: getPhotoTimestamp(photo) }))
    .filter((entry): entry is { photo: TimelinePhoto; date: Date } => entry.date !== null)
    .sort((a, b) => a.date.getTime() - b.date.getTime());

  if (!datedPhotos.length) {
    return {
      intervalMs: 15 * 60 * 1000,
      bins: [],
      start: null,
      end: null
    };
  }

  const dataStart = datedPhotos[0]!.date;
  const dataEnd = datedPhotos[datedPhotos.length - 1]!.date;
  const startDate = viewport ? new Date(viewport.start) : dataStart;
  const endDate = viewport ? new Date(viewport.end) : dataEnd;
  const spanMs = Math.max(1, endDate.getTime() - startDate.getTime());
  const intervalMs = getTimelineIntervalMs(spanMs);
  const domainStart = floorToInterval(startDate, intervalMs);
  const domainEnd = ceilToInterval(endDate, intervalMs);
  const bins: TimelineBin[] = [];

  for (let cursor = new Date(domainStart); cursor < domainEnd; cursor = new Date(cursor.getTime() + intervalMs)) {
    const next = new Date(cursor.getTime() + intervalMs);
    bins.push({
      start: new Date(cursor),
      end: next,
      count: 0,
      photoIds: [],
      label: formatTimelineLabel(cursor)
    });
  }

  const binIndexByStart = new Map<number, number>();
  bins.forEach((bin, index) => {
    binIndexByStart.set(bin.start.getTime(), index);
  });

  for (const entry of datedPhotos) {
    if (viewport && (entry.date.getTime() < viewport.start || entry.date.getTime() >= viewport.end)) {
      continue;
    }

    const bucketStart = floorToInterval(entry.date, intervalMs).getTime();
    const binIndex = binIndexByStart.get(bucketStart);
    if (binIndex !== undefined) {
      const bin = bins[binIndex];
      if (!bin) {
        continue;
      }
      bin.count += 1;
      bin.photoIds.push(entry.photo.id);
    }
  }

  return {
    intervalMs,
    bins,
    start: domainStart,
    end: domainEnd
  };
}
