import { activitySourceById } from "./registry";
import { safeReadSource } from "./read";
import {
  ACTIVITY_SEVERITY_ORDER,
  type ActivityEvent,
  type ActivitySourceEntriesOptions,
  type ActivitySourceEntriesResponse,
  type ActivitySourceCoverage,
} from "./types";

export const ACTIVITY_ENTRIES_DEFAULT_LIMIT = 100;
export const ACTIVITY_ENTRIES_MAX_LIMIT = 500;

function parseMs(value: string | undefined): number | undefined {
  if (!value) return undefined;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : undefined;
}

function clampLimit(value: number | undefined): number {
  if (value === undefined || !Number.isFinite(value)) return ACTIVITY_ENTRIES_DEFAULT_LIMIT;
  const clamped = Math.trunc(value);
  if (clamped < 1) return 1;
  if (clamped > ACTIVITY_ENTRIES_MAX_LIMIT) return ACTIVITY_ENTRIES_MAX_LIMIT;
  return clamped;
}

function clampOffset(value: number | undefined): number {
  if (value === undefined || !Number.isFinite(value)) return 0;
  const clamped = Math.trunc(value);
  return clamped < 0 ? 0 : clamped;
}

/**
 * Wider than the list page's title-only match: on a single source the reader has the
 * whole record, and a user searching one log expects its body to count.
 */
export function eventMatchesQuery(event: ActivityEvent, needle: string): boolean {
  const haystack = [
    event.title,
    event.category,
    event.actor,
    event.severity,
    event.project,
    event.host,
    event.runtime,
    event.session,
    event.dedupeKey,
    event.rc === undefined ? undefined : String(event.rc),
    event.detail === undefined ? undefined : JSON.stringify(event.detail),
  ];
  return haystack.some((field) => field !== undefined && field.toLowerCase().includes(needle));
}

/** `undefined` means the source id is not one this collector knows. */
export function readActivitySourceEntries(
  options: ActivitySourceEntriesOptions,
): ActivitySourceEntriesResponse | undefined {
  const descriptor = activitySourceById(options.sourceId);
  if (descriptor === undefined) return undefined;

  const fromMs = parseMs(options.from);
  const toMs = parseMs(options.to);
  const limit = clampLimit(options.limit);
  const requestedOffset = clampOffset(options.offset);
  const recordId = options.recordId?.trim();
  const offset = recordId ? 0 : requestedOffset;

  const prepared = descriptor.prepare({ fromMs, toMs, collectSkipped: true }, options.sources);
  const result = safeReadSource(prepared.fallback, prepared.read);
  const normalizedEvents = result.events.map((event) => descriptor.normalize(event, result.coverage.path));
  const source: ActivitySourceCoverage = { ...result.coverage, authority: descriptor.authority, storage: descriptor.storage, retention: descriptor.retention, queryBounds: descriptor.queryBounds };

  const needle = options.q?.trim().toLowerCase();
  const severityFloor = options.severityFloor;
  const matched = normalizedEvents.filter((event) => {
    if (recordId && event.id !== recordId) return false;
    const tsMs = Date.parse(event.ts);
    if (!Number.isFinite(tsMs)) return false;
    if (fromMs !== undefined && tsMs < fromMs) return false;
    if (toMs !== undefined && tsMs > toMs) return false;
    if (severityFloor && ACTIVITY_SEVERITY_ORDER[event.severity] < ACTIVITY_SEVERITY_ORDER[severityFloor]) return false;
    if (needle && needle.length > 0 && !eventMatchesQuery(event, needle)) return false;
    return true;
  });

  matched.sort((left, right) => {
    const delta = Date.parse(right.ts) - Date.parse(left.ts);
    if (delta !== 0) return delta;
    return left.id.localeCompare(right.id);
  });

  return {
    source,
    events: matched.slice(offset, offset + limit),
    total: matched.length,
    offset,
    limit,
    skipped: result.skippedEntries,
    skippedTotal: result.coverage.skipped,
    skippedTruncated: result.skippedTruncated,
  };
}
