import {
  type ActivityActor,
  type ActivityCategory,
  type ActivityEvent,
  ACTIVITY_SEVERITY_ORDER,
  type ActivityReadOptions,
  type ActivityReadResponse,
  type ActivitySeries,
  type ActivitySourceResult,
  type ActivitySourceCoverage,
  type ActivitySeverity,
  type SuppressedNotifications,
} from "./types";
import { ACTIVITY_SOURCES, type ActivitySourceFallback } from "./registry";
import { readSuppressedNotifications, defaultSuppressedPath } from "./sources/suppressed";

interface ResolvedFilters {
  categories: ActivityCategory[];
  projects: string[];
  runtimes: string[];
  hosts: string[];
  workloads: string[];
  builds: string[];
  actors: ActivityActor[];
  severityFloor?: ActivitySeverity;
  q?: string;
}

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 500;
  }
  const clamped = Math.trunc(value);
  if (clamped < 1) return 1;
  if (clamped > 2000) return 2000;
  return clamped;
}

function asErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export function errorSourceCoverage(fallback: ActivitySourceFallback, error: unknown): ActivitySourceResult {
  return {
    events: [],
    skippedEntries: [],
    skippedTruncated: false,
    coverage: {
      id: fallback.id,
      label: fallback.label,
      category: fallback.category,
      path: fallback.path,
      status: "error",
      records: 0,
      totalRecords: 0,
      skipped: 0,
      error: asErrorMessage(error),
    },
  };
}

export function safeReadSource(
  fallback: ActivitySourceFallback,
  readSource: () => ActivitySourceResult,
): ActivitySourceResult {
  try {
    return readSource();
  } catch (error) {
    return errorSourceCoverage(fallback, error);
  }
}

function readSuppressedOrError(path = defaultSuppressedPath()): SuppressedNotifications {
  try {
    return readSuppressedNotifications({ path });
  } catch (error) {
    return {
      status: "error",
      path,
      rows: [],
      sourceCount: 0,
      attemptCount: 0,
      skipped: 0,
      error: asErrorMessage(error),
    };
  }
}

function inRange(ms: number, fromMs?: number, toMs?: number): boolean {
  if (fromMs !== undefined && ms < fromMs) return false;
  if (toMs !== undefined && ms > toMs) return false;
  return true;
}

function applyFilters(result: ActivitySourceResult, filters: ResolvedFilters, fromMs?: number, toMs?: number) {
  return result.events.filter((event) => {
    const eventTsMs = Date.parse(event.ts);
    if (!Number.isFinite(eventTsMs) || !inRange(eventTsMs, fromMs, toMs)) return false;
    if (filters.categories.length > 0 && !filters.categories.includes(event.category)) return false;
    if (filters.projects.length > 0 && (event.project === undefined || !filters.projects.includes(event.project))) return false;
    if (filters.runtimes.length > 0 && (event.runtime === undefined || !filters.runtimes.includes(event.runtime))) return false;
    if (filters.hosts.length > 0 && (event.host === undefined || !filters.hosts.includes(event.host))) return false;
    if (filters.workloads.length > 0 && !filters.workloads.some((value) => event.detail?.workloadUid === value || event.detail?.workloadName === value)) return false;
    if (filters.builds.length > 0 && !filters.builds.some((value) => event.detail?.buildKey === value)) return false;
    if (filters.actors.length > 0 && !filters.actors.includes(event.actor)) return false;
    if (filters.severityFloor && ACTIVITY_SEVERITY_ORDER[event.severity] < ACTIVITY_SEVERITY_ORDER[filters.severityFloor]) return false;
    if (filters.q && !event.title.toLowerCase().includes(filters.q)) return false;
    return true;
  });
}

function buildCategoryCounts(events: ActivityEvent[]): Array<{ category: ActivityCategory; count: number }> {
  const counts = new Map<ActivityCategory, number>();

  for (const event of events) {
    counts.set(event.category, (counts.get(event.category) ?? 0) + 1);
  }
  return [...counts.entries()]
    .map(([category, count]) => ({ category, count }))
    .sort((left, right) => {
      if (right.count !== left.count) return right.count - left.count;
      return left.category.localeCompare(right.category);
    });
}

function buildProjectCounts(events: ActivityEvent[]): Array<{ project: string; count: number }> {
  const counts = new Map<string, number>();
  for (const event of events) {
    if (event.project) counts.set(event.project, (counts.get(event.project) ?? 0) + 1);
  }
  return [...counts.entries()].map(([project, count]) => ({ project, count })).sort((left, right) => left.project.localeCompare(right.project));
}

const SERIES_LABELS: Record<ActivitySeries["id"], string> = {
  activeAgents: "Active agents",
  logVolume: "Log volume",
  commits: "Commits",
  bots: "Bots",
  builds: "Builds",
  incidents: "Incidents",
};

export function buildSeries(events: ActivityEvent[], coverage: ActivityReadResponse["coverage"], capabilities: Map<ActivitySeries["id"], Set<string>>, fromMs?: number, toMs?: number, canonicalIntervalEvents: ActivityEvent[] = events): ActivitySeries[] {
  const timestamps = events.map((event) => Date.parse(event.ts)).filter(Number.isFinite);
  const start = fromMs ?? (timestamps.length > 0 ? Math.min(...timestamps) : undefined);
  const end = toMs ?? (timestamps.length > 0 ? Math.max(...timestamps) : undefined);
  const bucketMs = start === undefined || end === undefined ? undefined : Math.max(60_000, Math.ceil((end - start + 1) / 48));
  return (Object.keys(SERIES_LABELS) as ActivitySeries["id"][]).map((id) => {
    const sourceIds = capabilities.get(id) ?? new Set<string>();
    const capableCoverage = coverage.filter((source) => sourceIds.has(source.id));
    const healthy = capableCoverage.some((source) => source.status === "ok");
    const failedSources = capableCoverage.filter((source) => source.status !== "ok");
    if (!healthy || failedSources.length > 0 || start === undefined || end === undefined || bucketMs === undefined) {
      const reason = failedSources.length > 0
        ? `Coverage incomplete: ${failedSources.map((source) => `${source.label} (${source.status})`).join(", ")}.`
        : healthy ? "No query interval is available." : "No healthy source authoritatively records this series.";
      return { id, label: SERIES_LABELS[id], status: "unavailable", reason, points: [] };
    }
    const points: Array<{ ts: string; value: number }> = [];
    if (id === "activeAgents") {
      const intervals = new Map<string, { start?: number; end?: number }>();
      for (const event of canonicalIntervalEvents.filter((item) => sourceIds.has(item.source) && item.session && (toMs === undefined || Date.parse(item.ts) <= toMs))) {
        const interval = intervals.get(event.session!) ?? {};
        const ts = Date.parse(event.ts);
        if (event.lifecycle === "started" || event.lifecycle === "active") interval.start = Math.min(interval.start ?? ts, ts);
        if (event.lifecycle === "completed" || event.lifecycle === "failed" || event.lifecycle === "cancelled") interval.end = Math.max(interval.end ?? ts, ts);
        intervals.set(event.session!, interval);
      }
      const bounded = [...intervals.values()]
        .filter((interval): interval is { start: number; end?: number } => interval.start !== undefined)
        .map((interval) => ({ start: interval.start, end: interval.end ?? end }));
      if (bounded.length === 0) return { id, label: SERIES_LABELS[id], status: "unavailable", reason: "Session start intervals are unavailable.", points: [] };
      const boundaries = bounded.flatMap((interval) => [[interval.start, 1] as const, [interval.end, -1] as const]).sort((left, right) => left[0] - right[0] || left[1] - right[1]);
      let boundaryIndex = 0;
      let active = 0;
      for (let bucketStart = start; bucketStart <= end; bucketStart += bucketMs) {
        const bucketEnd = Math.min(end + 1, bucketStart + bucketMs);
        while (boundaryIndex < boundaries.length && boundaries[boundaryIndex]![0] < bucketStart) active += boundaries[boundaryIndex++]![1];
        let peak = active;
        while (boundaryIndex < boundaries.length && boundaries[boundaryIndex]![0] < bucketEnd) {
          active += boundaries[boundaryIndex++]![1];
          peak = Math.max(peak, active);
        }
        points.push({ ts: new Date(bucketStart).toISOString(), value: peak });
      }
    } else {
      const coverageStarts = capableCoverage.map((source) => source.coverageFrom ? Date.parse(source.coverageFrom) : Number.NaN);
      if (coverageStarts.some((value) => !Number.isFinite(value))) {
        return { id, label: SERIES_LABELS[id], status: "unavailable", reason: "Authoritative retained coverage is unknown for one or more contributing sources.", points: [] };
      }
      const coverageStart = Math.max(start, ...coverageStarts);
      for (let ts = coverageStart; ts <= end; ts += bucketMs) points.push({ ts: new Date(ts).toISOString(), value: 0 });
      for (const event of events.filter((item) => sourceIds.has(item.source))) {
        const index = Math.min(points.length - 1, Math.floor((Date.parse(event.ts) - coverageStart) / bucketMs));
        if (index >= 0) points[index]!.value += 1;
      }
      const gaps = coverageStart > start ? [{ from: new Date(start).toISOString(), to: new Date(coverageStart).toISOString(), reason: "Source retention does not cover this interval." }] : undefined;
      return { id, label: SERIES_LABELS[id], status: "available", coverageFrom: new Date(coverageStart).toISOString(), ...(gaps ? { gaps } : {}), points };
    }
    return { id, label: SERIES_LABELS[id], status: "available", points };
  });
}

function buildSourceCounts(events: ActivityEvent[]): Array<{ source: string; count: number }> {
  const counts = new Map<string, number>();
  for (const event of events) {
    counts.set(event.source, (counts.get(event.source) ?? 0) + 1);
  }
  return [...counts.entries()]
    .map(([source, count]) => ({ source, count }))
    .sort((left, right) => {
      if (right.count !== left.count) return right.count - left.count;
      return left.source.localeCompare(right.source);
    });
}

function normalizeFilters(options: ActivityReadOptions): { fromMs?: number; toMs?: number; filters: ResolvedFilters } {
  return {
    fromMs: parseMs(options.from),
    toMs: parseMs(options.to),
    filters: {
      categories: options.categories ?? [],
      projects: options.projects ?? [],
      runtimes: options.runtimes ?? [],
      hosts: options.hosts ?? [],
      workloads: options.workloads ?? [],
      builds: options.builds ?? [],
      actors: options.actors ?? [],
      severityFloor: options.severityFloor,
      q: options.q?.trim().toLowerCase(),
    },
  };
}

const ACTIVITY_CACHE_TTL_MS = 20_000;
type NormalizedSource = Omit<ActivitySourceResult, "coverage"> & { coverage: ActivitySourceCoverage };
let sourceSnapshot: { expiresAt: number; sources: NormalizedSource[] } | undefined;

export function clearActivityCache(): void {
  sourceSnapshot = undefined;
}

function readNormalizedSources(options: ActivityReadOptions, fromMs?: number, toMs?: number, now = Date.now): NormalizedSource[] {
  const timestamp = now();
  if (options.sources === undefined && sourceSnapshot && sourceSnapshot.expiresAt > timestamp) return sourceSnapshot.sources;
  const sources = ACTIVITY_SOURCES.map((descriptor) => {
    const sourceWindow = options.sources === undefined || descriptor.id === "agent-sessions" ? {} : { fromMs, toMs };
    const prepared = descriptor.prepare(sourceWindow, options.sources);
    const result = safeReadSource(prepared.fallback, prepared.read);
    return {
      ...result,
      events: result.events.map((event) => descriptor.normalize(event, result.coverage.path)),
      coverage: { ...result.coverage, authority: descriptor.authority, storage: descriptor.storage, retention: descriptor.retention, queryBounds: descriptor.queryBounds },
    };
  });
  if (options.sources === undefined) sourceSnapshot = { expiresAt: timestamp + ACTIVITY_CACHE_TTL_MS, sources };
  return sources;
}

export function readActivityCached(options: ActivityReadOptions = {}): ActivityReadResponse {
  return readActivity(options);
}

export function readActivity(options: ActivityReadOptions = {}): ActivityReadResponse {
  const { fromMs, toMs, filters } = normalizeFilters(options);
  const normalizedSources = readNormalizedSources(options, fromMs, toMs);

  const events = normalizedSources.flatMap((result) => applyFilters(result, filters, fromMs, toMs));
  const facetFilters: ResolvedFilters = { ...filters, categories: [] };
  const facetEvents = normalizedSources.flatMap((result) => applyFilters(result, facetFilters, fromMs, toMs));
  const categoryCounts = buildCategoryCounts(facetEvents);
  const sourceCounts = buildSourceCounts(facetEvents);
  const projectCounts = buildProjectCounts(normalizedSources.flatMap((result) => applyFilters(result, { ...filters, projects: [] }, fromMs, toMs)));
  const coverage = normalizedSources.map((source) => source.coverage);
  const capabilities = new Map<ActivitySeries["id"], Set<string>>();
  for (const descriptor of ACTIVITY_SOURCES) for (const id of descriptor.series) (capabilities.get(id) ?? capabilities.set(id, new Set()).get(id)!).add(descriptor.id);
  const intervalFilters: ResolvedFilters = { ...filters, categories: [], severityFloor: undefined, q: undefined };
  const canonicalIntervalEvents = normalizedSources.flatMap((source) => applyFilters(source, intervalFilters, undefined, toMs));
  const series = buildSeries(events, coverage, capabilities, fromMs, toMs, canonicalIntervalEvents);

  events.sort((left, right) => {
    const delta = Date.parse(right.ts) - Date.parse(left.ts);
    if (delta !== 0) return delta;
    return left.id.localeCompare(right.id);
  });
  const total = events.length;
  const limit = clampLimit(options.limit);
  const limited = events.slice(0, limit);

  return {
    events: limited,
    total,
    truncated: total > limited.length,
    limit,
    categoryCounts,
    projectCounts,
    sourceCounts,
    errorCount: events.filter((event) => event.severity === "error").length,
    series,
    coverage,
    suppressedNotifications: readSuppressedOrError(),
  };
}
