import type { ConsoleAccessResolver } from "../auth/access";
// Telemetry domain: system health, usage, and telemetry-event read API.

import { ConsoleDomainError, requireTenantScope } from "../shared/errors";
import { parseQueryLimit } from "../shared/query";
import type { AccessDecision } from "../../security/access-control";
import { USAGE_DIMENSIONS, type UsageDimension } from "./usage-dimensions";

/**
 * Whitelisted usage periods. Numeric tokens bound the aggregation window;
 * `"all"` is the single explicit unbounded token. Anything else is rejected
 * before any database query so a typo cannot trigger a full-table scan.
 */
export function isSupportedUsagePeriod(period: string): boolean {
  if (period === "all") return true;
  const match = /^(\d+)([hd])$/.exec(period);
  if (!match) return false;
  const amount = Number(match[1]);
  const cap = match[2] === "h" ? 8760 : 365;
  return Number.isSafeInteger(amount) && amount >= 1 && amount <= cap;
}

export { USAGE_PERIODS, type UsagePeriod } from "./usage-periods";

export interface SystemHealthResponse {
  version: string;
  status: "healthy" | "degraded" | "unhealthy";
  uptime_seconds: number;
  database_healthy: boolean;
  redis_healthy: boolean;
  /** False when no Redis client is configured and coordination stays in memory. */
  redis_configured: boolean;
  /** JavaScript heap used by this process. */
  memory_heap_bytes: number;
  /** Native buffers and malloc outside the JavaScript heap. */
  memory_external_bytes: number;
  memory_bytes: number;
  /** RSS as a percentage of system memory, not of the JS heap. */
  memory_percent: number;
  /**
   * True when the database runs inside this process (SQLite). Then RSS covers
   * the gateway *and* a whole PostgreSQL build, so it is not comparable with a
   * driver-only process and the console must say so rather than imply the
   * gateway itself grew.
   */
  database_in_process: boolean;
  cpu_percent: number;
  cpu_cores: number;
  pid: number;
  platform: string;
  request_count: number;
  error_count: number;
  latency_avg_ms: number;
  latency_p95_ms: number;
  latency_p99_ms: number;
  cache_hit_rate_percent: number;
  avg_tokens_per_sec: number;
}
export interface UsageResponse {
  tenantId: string;
  period: string;
  requestsTotal: number;
  requestsSucceeded: number;
  requestsFailed: number;
  tokensUsed: number;
  estimatedCost: number;
  topModels: Array<{ modelId: string; count: number }>;
  topProviders: Array<{ providerId: string; count: number }>;
}

/** Period-bound usage summary mirroring the 20-beta analytics surface. */
export interface UsageSummaryTotals {
  requests: number;
  inputTokens: number;
  cachedTokens: number;
  outputTokens: number;
  /**
   * Failed and truncated requests. Client aborts (`cancelled`, rendered 499)
   * are expected client behavior, never an error, and are tracked separately.
   */
  errors: number;
  /** Client-cancelled requests (rendered `499`) — a sub-count, never an error. */
  cancelled: number;
  /** Streams that ended early (`502`); also included in `errors`. */
  truncated: number;
  /** Counts by actual HTTP status for the clickable Requests filters. */
  statusCounts: Array<{ status: number; count: number }>;
  /** Average gateway routing and preparation delay before upstream dispatch. */
  avgResolveMs: number;
  avgDurationMs: number;
  /** Nearest-rank latency percentiles over the selected usage period. */
  latencyP50Ms: number;
  latencyP95Ms: number;
  latencyP99Ms: number;
  /**
   * How many events each latency series was computed from. A percentile over
   * twenty requests and one over twenty thousand do not deserve the same
   * confidence, and the count is the only thing that says which it is.
   */
  latencySampleCount: number;
  /** Events carrying a resolve measurement; differs when a row never dispatched. */
  resolveSampleCount: number;
  estimatedCostUsd: number;
  /** True when completed rows without persisted cost exist in the window. */
  partial: boolean;
  /** Cache hit rate percentage (0-100). */
  cacheHitRate: number;
  /** Average tokens per second across the period. */
  avgTokensPerSec: number;
}
export interface UsageSummaryResponse {
  period: string;
  totals: UsageSummaryTotals;
}
export interface UsageChartBucket {
  /** Bucket start as ISO timestamp. */
  t: string;
  requests: number;
  input: number;
  cached: number;
  output: number;
}
export interface UsageChartResponse {
  buckets: UsageChartBucket[];
}
export interface UsageByRow {
  name: string;
  requests: number;
  input: number;
  output: number;
  cached: number;
  total: number;
  errors: number;
  /** Null when no row in the group carries persisted cost. */
  costUsd: number | null;
  /** Resolved API key label for the `key` dimension; absent otherwise. */
  label?: string;
  /** Cache hit rate percentage (0-100). */
  cacheHitRate: number;
  /** Average tokens per second for this dimension. */
  avgTokensPerSec: number;
}
/**
 * Breakdown dimensions the usage endpoint accepts. Declared in `./usage-dimensions`
 * and re-exported here, because three layers read the same binding: the route
 * table registers one `by-<dimension>` path per member, the operations validator
 * rejects anything outside it, and the dashboard's `Dimension` union mirrors it
 * (kept in sync by hand).
 */
export { USAGE_DIMENSIONS, type UsageDimension };

export interface UsageByResponse {
  /** The dimension these rows were grouped by, so a stale response can never be read as another tab's data. */
  dimension: UsageDimension;
  rows: UsageByRow[];
}
export interface UsageCacheResponse {
  period: string;
  inputTokens: number;
  cachedTokens: number;
  /** Always zero: cache-write tokens are not tracked by the telemetry schema. */
  cacheWriteTokens: number;
  hitRate: number;
}
export type UsageRequestMode = "stream" | "non_stream";
export interface UsageRequestItem {
  requestId: string;
  surface?: string;
  providerId?: string;
  /** Provider account (credential) that served this request, if any. */
  accountId?: string;
  model?: string;
  /**
   * The model the upstream named in its own response, verbatim — not the one
   * the client asked for. Absent when the response carried no `model`; never
   * backfilled from `model`, because that would report the ask as the answer.
   */
  reportedModel?: string;
  endpoint?: string;
  apiKeyId?: string;
  userAgent?: string;
  clientName?: string;
  estimatedCost?: number;
  /** Provider-billed credits; absent when the upstream reported no credit field. */
  creditUsed?: number;
  /** Gateway-side time from request start to upstream dispatch, in ms. */
  resolveMs?: number;
  /** Resolved API key label; absent when the key is gone or unknown. */
  apiKeyLabel?: string;
  /** HTTP status the client actually received; absent on historical rows. */
  httpStatus?: number;
  status: string;
  errorKind?: string;
  /**
   * Which layer failed: `cartethyia` (the gateway), `upstream` (the provider),
   * or `network`. Absent for rows written before the column existed.
   */
  errorOrigin?: string;
  mode: UsageRequestMode;
  startedAt: string;
  clientIp?: string;
  durationMs?: number;
  inputTokens?: number;
  outputTokens?: number;
  cachedTokens?: number;
  reasoningTokens?: number;
  totalTokens?: number;
  ttfbMs?: number;
  tokensPerSec?: number;
  firstContentDeltaAtMs?: number;
  lastEventAtMs?: number;
  /**
   * Why this request's drawer payloads are empty, or `captured` when they are
   * not. Remote-diagnosis field: the four panels read blank for every reason
   * alike, so without this an operator cannot tell "capture is off" from
   * "capture is broken" without access to the server's logs. Absent on rows
   * written before the column existed.
   */
  payloadCaptureState?: string;
}
export interface UsageRequestsResponse {
  items: UsageRequestItem[];
}
export interface UsageRequestDetail extends UsageRequestItem {
  proxy?: string;
  payloads?: {
    request?: unknown;
    response?: unknown;
    /** Body Cartethyia returned to the client (post-translation). */
    clientResponse?: unknown;
    providerRequest?: unknown;
    providerResponse?: unknown;
  } | null;
  payloadSignals?: {
    toolCalls: number;
    images: number;
    attachments: number;
  };
}
export interface TelemetryEventView {
  id: string;
  createdAt: string;
  tenantId: string;
  requestId: string;
  sourceSurface?: string;
  requestedModel?: string;
  providerId?: string;
  latencyMs?: number;
  status?: string;
  inputTokens?: number;
  outputTokens?: number;
  estimatedCost?: number;
  firstContentDeltaAtMs?: number;
  lastEventAtMs?: number;
}
export interface TelemetryEventDetail extends TelemetryEventView {
  payload?: { requestBody?: unknown; responseBody?: unknown };
}
export interface TelemetryEventListPage {
  entries: readonly TelemetryEventView[];
  /** Opaque cursor for the next page; `undefined` when the tail is reached. */
  nextCursor?: string;
}
export interface ObservabilityStore {
  health(tenantId: string): Promise<SystemHealthResponse>;
  usage(tenantId: string, period: string): Promise<UsageResponse>;
  listEvents(tenantId: string, limit: number, cursor?: string): Promise<TelemetryEventListPage>;
  getEvent(
    tenantId: string,
    eventId: string,
    includePayload: boolean,
    createdAt?: string,
  ): Promise<TelemetryEventDetail | undefined>;
  usageSummary(tenantId: string, period: string): Promise<UsageSummaryResponse>;
  usageChart(tenantId: string, period: string): Promise<UsageChartResponse>;
  usageBy(
    tenantId: string,
    dimension: UsageDimension,
    period: string,
  ): Promise<UsageByResponse>;
  usageCache(tenantId: string, period: string): Promise<UsageCacheResponse>;
  usageRequests(
    tenantId: string,
    period: string,
    limit: number,
    httpStatus?: number,
  ): Promise<UsageRequestsResponse>;
  usageRequestDetail(tenantId: string, requestId: string): Promise<UsageRequestDetail | undefined>;
}
export interface ObservabilityConfig {
  readonly store: ObservabilityStore;
  readonly accessResolver: ConsoleAccessResolver;
}

export function createObservabilityOperations(config: ObservabilityConfig) {
  const operations = {
    async getSystemHealth(access: AccessDecision | undefined): Promise<SystemHealthResponse> {
        const a = requireTenantScope(access, "dashboard:read");
        const h = await config.store.health(a.tenantId);
        return h;
      },
    async getTenantUsage(access: AccessDecision | undefined, period = "7d"): Promise<UsageResponse> {
        const a = requireTenantScope(access, "dashboard:read");
        if (!isSupportedUsagePeriod(period)) {
          throw new ConsoleDomainError(
            "invalid_period",
            400,
            `Unsupported usage period: ${period}. Use "24h", "7d", "30d", or "all".`,
          );
        }
        return config.store.usage(a.tenantId, period);
      },
    async listEvents(
        access: AccessDecision | undefined,
        limit?: string,
        cursor?: string,
      ): Promise<TelemetryEventListPage> {
        const a = requireTenantScope(access, "dashboard:read");
        const lim = parseQueryLimit(limit, 50, 100);
        const page = await config.store.listEvents(a.tenantId, lim, cursor);
        const entries = page.entries;
        return page.nextCursor ? { entries, nextCursor: page.nextCursor } : { entries };
      },
    async getEventDetail(
        access: AccessDecision | undefined,
        eventId: string,
        includePayload = false,
        createdAt?: string,
      ): Promise<TelemetryEventDetail> {
        const a = requireTenantScope(access, "dashboard:read");
        const detail = await config.store.getEvent(
          a.tenantId,
          eventId,
          includePayload,
          createdAt,
        );
        if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Event ${eventId} not found`);
        return detail;
      },
    requirePeriod(period: string): string {
        if (!isSupportedUsagePeriod(period)) {
          throw new ConsoleDomainError(
            "invalid_period",
            400,
            `Unsupported usage period: ${period}. Use "1h", "24h", "7d", "30d", or "all".`,
          );
        }
        return period;
      },
    requireTenant(access: AccessDecision | undefined): string {
        const a = requireTenantScope(access, "dashboard:read");
        return a.tenantId;
      },
    async getUsageSummary(
        access: AccessDecision | undefined,
        period = "24h",
      ): Promise<UsageSummaryResponse> {
        const tenantId = operations.requireTenant(access);
        return config.store.usageSummary(tenantId, operations.requirePeriod(period));
      },
    async getUsageChart(
        access: AccessDecision | undefined,
        period = "24h",
      ): Promise<UsageChartResponse> {
        const tenantId = operations.requireTenant(access);
        return config.store.usageChart(tenantId, operations.requirePeriod(period));
      },
    async getUsageBy(
        access: AccessDecision | undefined,
        dimension: UsageDimension,
        period = "24h",
      ): Promise<UsageByResponse> {
        const tenantId = operations.requireTenant(access);
        if (!(USAGE_DIMENSIONS as readonly string[]).includes(dimension)) {
          throw new ConsoleDomainError(
            "invalid_dimension",
            400,
            `Unsupported usage dimension: ${dimension}. Use ${USAGE_DIMENSIONS.map((d) => `"${d}"`).join(", ")}.`,
          );
        }
        return config.store.usageBy(tenantId, dimension, operations.requirePeriod(period));
      },
    async getUsageCache(
        access: AccessDecision | undefined,
        period = "24h",
      ): Promise<UsageCacheResponse> {
        const tenantId = operations.requireTenant(access);
        return config.store.usageCache(tenantId, operations.requirePeriod(period));
      },
    async listUsageRequests(
      access: AccessDecision | undefined,
      period = "24h",
      limit?: string,
      httpStatus?: string,
    ): Promise<UsageRequestsResponse> {
      const tenantId = operations.requireTenant(access);
      let parsedStatus: number | undefined;
      if (httpStatus !== undefined) {
        parsedStatus = Number(httpStatus);
        if (!Number.isInteger(parsedStatus) || parsedStatus < 100 || parsedStatus > 599) {
          throw new ConsoleDomainError("invalid_request", 400, "Invalid HTTP status filter");
        }
      }
      return config.store.usageRequests(
        tenantId,
        operations.requirePeriod(period),
        parseQueryLimit(limit, 50, 500),
        parsedStatus,
      );
    },
    async getUsageRequestDetail(
        access: AccessDecision | undefined,
        requestId: string,
      ): Promise<UsageRequestDetail> {
        const tenantId = operations.requireTenant(access);
        const detail = await config.store.usageRequestDetail(tenantId, requestId);
        if (!detail) throw new ConsoleDomainError("event_not_found", 404, `Request ${requestId} not found`);
        const payloads = detail.payloads ?? null;
        return { ...detail, payloads };
      },
  };
  return operations;
}
