import type { DatabaseAdapter } from "../db.js";
import { ValidationError, assertAllowedKeys } from "../errors.js";
import { ALL_SOURCES, type AnalyticsSource } from "../sources.js";
import { parseOptionalEnumArray } from "../validation.js";

interface TimeseriesInput {
  sources?: AnalyticsSource[];
  date_from?: string;
  date_to?: string;
  model?: string;
  include_technical?: boolean;
}

interface Bucket {
  date: string;
  sessions: number;
  messages: number;
  events: number;
  input_tokens: number;
  output_tokens: number;
}

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
const RFC3339_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})$/;
const MAX_DENSE_RANGE_DAYS = 3660;

function parseIso(value: unknown, field: "date_from" | "date_to"): string | undefined {
  if (value === undefined) return undefined;
  if (typeof value !== "string") {
    throw new ValidationError(`${field} must be an ISO date or RFC 3339 timestamp.`);
  }

  const trimmed = value.trim();
  const normalized = ISO_DATE.test(trimmed)
    ? `${trimmed}T${field === "date_to" ? "23:59:59.999" : "00:00:00.000"}Z`
    : trimmed;
  if ((!ISO_DATE.test(trimmed) && !RFC3339_DATE_TIME.test(trimmed)) || !Number.isFinite(Date.parse(normalized))) {
    throw new ValidationError(`${field} must be an ISO date or RFC 3339 timestamp.`);
  }
  return new Date(normalized).toISOString();
}

function parseInput(args: unknown): TimeseriesInput {
  const obj = (args && typeof args === "object" && !Array.isArray(args) ? args : {}) as Record<string, unknown>;
  assertAllowedKeys(obj, ["sources", "date_from", "date_to", "model", "include_technical"]);

  const sources = parseOptionalEnumArray(obj.sources, ALL_SOURCES, "sources");
  if (obj.model !== undefined && (typeof obj.model !== "string" || obj.model.trim().length === 0)) {
    throw new ValidationError("model must be a non-empty string.");
  }
  if (obj.include_technical !== undefined && typeof obj.include_technical !== "boolean") {
    throw new ValidationError("include_technical must be a boolean.");
  }

  const dateFrom = parseIso(obj.date_from, "date_from");
  const dateTo = parseIso(obj.date_to, "date_to");
  if (dateFrom && dateTo && Date.parse(dateFrom) > Date.parse(dateTo)) {
    throw new ValidationError("date_from must be less than or equal to date_to.");
  }

  return {
    sources,
    date_from: dateFrom,
    date_to: dateTo,
    model: typeof obj.model === "string" ? obj.model.trim() : undefined,
    include_technical: obj.include_technical === true
  };
}

// Effective model for a message follows the same precedence used by `analytics_models`:
// the message-level model when present, otherwise the session's primary model.
const EFFECTIVE_MODEL_EXPR = "COALESCE(NULLIF(mm.model,''), NULLIF(s.model_primary,''))";

function emptyBucket(date: string): Bucket {
  return { date, sessions: 0, messages: 0, events: 0, input_tokens: 0, output_tokens: 0 };
}

function seedDenseRange(buckets: Map<string, Bucket>, dateFrom?: string, dateTo?: string): boolean {
  if (!dateFrom || !dateTo) return false;
  const start = new Date(`${dateFrom.slice(0, 10)}T00:00:00.000Z`);
  const end = new Date(`${dateTo.slice(0, 10)}T00:00:00.000Z`);
  const dayCount = Math.floor((end.getTime() - start.getTime()) / 86400000) + 1;
  if (dayCount < 1 || dayCount > MAX_DENSE_RANGE_DAYS) return false;

  for (let cursor = start.getTime(); cursor <= end.getTime(); cursor += 86400000) {
    const date = new Date(cursor).toISOString().slice(0, 10);
    buckets.set(date, emptyBucket(date));
  }
  return true;
}

export async function queryTimeseries(db: DatabaseAdapter, args: unknown): Promise<Record<string, unknown>> {
  const input = parseInput(args);
  const sources = input.sources ?? [...ALL_SOURCES];
  const sessionSources = sources.filter((source) => source !== "hook_log");
  const eventSources = sources;

  // Session-kind scope mirrors `analytics_summary`'s fixed KPI definition (main sessions with at
  // least one user message) by default. `include_technical=true` widens the session/message/token
  // scope to every session_kind. Runtime events have no session_kind and therefore are unaffected.
  const sessionKindClause: string[] = input.include_technical
    ? []
    : ["COALESCE(s.session_kind, 'main') = 'main'", "s.user_message_count > 0"];

  const sessionScopeWhere: string[] = [];
  const sessionScopeParams: unknown[] = [];
  if (sessionSources.length > 0) {
    sessionScopeWhere.push(`s.source IN (${sessionSources.map(() => "?").join(",")})`);
    sessionScopeParams.push(...sessionSources);
  } else {
    sessionScopeWhere.push("1=0");
  }
  sessionScopeWhere.push(...sessionKindClause);

  const sessionDateWhere: string[] = [];
  const sessionDateParams: unknown[] = [];
  if (input.date_from) {
    sessionDateWhere.push("s.updated_at >= ?");
    sessionDateParams.push(input.date_from);
  }
  if (input.date_to) {
    sessionDateWhere.push("s.updated_at <= ?");
    sessionDateParams.push(input.date_to);
  }

  const messageDateWhere: string[] = ["mm.created_at IS NOT NULL"];
  const messageDateParams: unknown[] = [];
  if (input.date_from) {
    messageDateWhere.push("mm.created_at >= ?");
    messageDateParams.push(input.date_from);
  }
  if (input.date_to) {
    messageDateWhere.push("mm.created_at <= ?");
    messageDateParams.push(input.date_to);
  }

  const modelWhere: string[] = [];
  const modelParams: unknown[] = [];
  if (input.model) {
    modelWhere.push(`${EFFECTIVE_MODEL_EXPR} = ?`);
    modelParams.push(input.model);
  }

  const sessionWhere = [...sessionScopeWhere, ...sessionDateWhere];
  const sessionParams = [...sessionScopeParams, ...sessionDateParams];

  // Sessions are bucketed and filtered by their last-activity timestamp (`sessions.updated_at`).
  const sessionRows = input.model
    ? await db.all<{ date: string; sessions: number }>(
        `SELECT substr(s.updated_at,1,10) as date, COUNT(DISTINCT s.id) as sessions
         FROM sessions s JOIN message_metrics mm ON mm.session_id = s.id
         WHERE ${[...sessionWhere, ...modelWhere].join(" AND ")}
         GROUP BY date`,
        [...sessionParams, ...modelParams]
      )
    : await db.all<{ date: string; sessions: number }>(
        `SELECT substr(s.updated_at,1,10) as date, COUNT(*) as sessions
         FROM sessions s
         WHERE ${sessionWhere.join(" AND ")}
         GROUP BY date`,
        sessionParams
      );

  // Messages are bucketed and filtered by their own creation timestamp. Do not reuse the session
  // date predicate here: doing so includes out-of-range messages from sessions updated in range and
  // excludes in-range messages from sessions updated after date_to.
  const messageWhere = [...sessionScopeWhere, ...messageDateWhere, "mm.role = 'user'", ...modelWhere];
  const messageParams = [...sessionScopeParams, ...messageDateParams, ...modelParams];
  const messageRows = await db.all<{ date: string; messages: number }>(
    `SELECT substr(mm.created_at,1,10) as date, COUNT(*) as messages
     FROM sessions s JOIN message_metrics mm ON mm.session_id = s.id
     WHERE ${messageWhere.join(" AND ")}
     GROUP BY date`,
    messageParams
  );

  // Tokens are bucketed and filtered by message creation timestamp, matching the row that owns the
  // token counters. Observed tokens are input_tokens + output_tokens.
  const tokenWhere = [...sessionScopeWhere, ...messageDateWhere, ...modelWhere];
  const tokenParams = [...sessionScopeParams, ...messageDateParams, ...modelParams];
  const tokenRows = await db.all<{ date: string; input_tokens: number; output_tokens: number }>(
    `SELECT substr(mm.created_at,1,10) as date,
       COALESCE(SUM(COALESCE(mm.input_tokens,0)),0) as input_tokens,
       COALESCE(SUM(COALESCE(mm.output_tokens,0)),0) as output_tokens
     FROM sessions s JOIN message_metrics mm ON mm.session_id = s.id
     WHERE ${tokenWhere.join(" AND ")}
     GROUP BY date`,
    tokenParams
  );

  // Events are bucketed and filtered by occurrence timestamp. Self events are excluded, matching
  // `analytics_summary`. `model` and `include_technical` do not apply because runtime events are not
  // associated with a message model or session_kind.
  const eventWhere: string[] = [];
  const eventParams: unknown[] = [];
  if (eventSources.length > 0) {
    eventWhere.push(`source IN (${eventSources.map(() => "?").join(",")})`);
    eventParams.push(...eventSources);
  } else {
    eventWhere.push("1=0");
  }
  if (input.date_from) {
    eventWhere.push("occurred_at >= ?");
    eventParams.push(input.date_from);
  }
  if (input.date_to) {
    eventWhere.push("occurred_at <= ?");
    eventParams.push(input.date_to);
  }
  eventWhere.push("is_self_event = 0");
  const eventRows = await db.all<{ date: string; events: number }>(
    `SELECT substr(occurred_at,1,10) as date, COUNT(*) as events
     FROM runtime_events
     WHERE ${eventWhere.join(" AND ")}
     GROUP BY date`,
    eventParams
  );

  const buckets = new Map<string, Bucket>();
  const denseRange = seedDenseRange(buckets, input.date_from, input.date_to);
  const ensure = (date: string): Bucket => {
    let bucket = buckets.get(date);
    if (!bucket) {
      bucket = emptyBucket(date);
      buckets.set(date, bucket);
    }
    return bucket;
  };

  for (const row of sessionRows) {
    if (!row.date) continue;
    ensure(row.date).sessions = row.sessions;
  }
  for (const row of messageRows) {
    if (!row.date) continue;
    ensure(row.date).messages = row.messages;
  }
  for (const row of tokenRows) {
    if (!row.date) continue;
    const bucket = ensure(row.date);
    bucket.input_tokens = row.input_tokens;
    bucket.output_tokens = row.output_tokens;
  }
  for (const row of eventRows) {
    if (!row.date) continue;
    ensure(row.date).events = row.events;
  }

  const orderedBuckets = [...buckets.values()]
    .sort((left, right) => left.date.localeCompare(right.date))
    .map((bucket) => ({ ...bucket, observed_tokens: bucket.input_tokens + bucket.output_tokens }));

  return {
    ok: true,
    granularity: "day",
    timezone: "UTC",
    dense_range: denseRange,
    range: {
      date_from: input.date_from ?? null,
      date_to: input.date_to ?? null,
      inclusive: true
    },
    bucket_fields: {
      sessions: "sessions.updated_at",
      messages: "message_metrics.created_at",
      tokens: "message_metrics.created_at",
      events: "runtime_events.occurred_at"
    },
    filters_not_applicable_to_events: ["model", "include_technical"],
    buckets: orderedBuckets
  };
}
