export type PolygonRunState =
  | "admitted"
  | "starting"
  | "running"
  | "paused"
  | "stopping"
  | "completed"
  | "failed"
  | "aborted";

export interface PolygonRunSummary {
  runId: string;
  episodeId: string;
  kind: string;
  state: PolygonRunState;
  createdAtUtc: string;
  startedAtUtc: string | null;
  endedAtUtc: string | null;
  terminalReason: string | null;
  scenarioGeneration: string;
  profileGeneration: string;
  missionCoreCommit: string;
  hostProfileId: string;
  reproducibilityTier: "R0" | "R1" | "R2";
  clockDomain: string;
  revision: number;
  providerIds: string[];
  artifactCount: number;
}

export interface PolygonProviderPin {
  identifier: string;
  version: string;
  revision: string;
  digest: string | null;
}

export interface PolygonAuthority {
  generation: number;
  commandTtlMaxNs: number;
  heartbeatTimeoutMonotonicNs: number;
  simulationOrShadowOnly: boolean;
  actuatorAuthority: boolean;
  navigationOrSafetyAccepted: boolean;
  directActuatorSetpointsAllowed: boolean;
}

export interface PolygonRun extends PolygonRunSummary {
  scenarioSha256: string;
  profileSha256: string;
  hostProfileSha256: string;
  seed: number;
  parentRunId: string | null;
  providers: PolygonProviderPin[];
  authority: PolygonAuthority;
}

export interface PolygonRunEvent {
  runId: string;
  sequence: number;
  eventType: string;
  observedAtUtc: string;
  hostMonotonicNs: number;
  simTimeNs: number | null;
  payload: Record<string, unknown>;
}

export interface PolygonRunArtifact {
  artifactId: string;
  kind: string;
  relativePath: string;
  sha256: string;
  byteLength: number;
  sourceOfRecord: boolean;
  sequence: number;
}

export interface PolygonRunCatalog {
  items: PolygonRunSummary[];
  total: number;
  limitations: string[];
}

export interface PolygonRunDetail {
  run: PolygonRun;
  events: PolygonRunEvent[];
  eventsTotal: number;
  eventsTruncated: boolean;
  commandCount: number;
  artifacts: PolygonRunArtifact[];
  limitations: string[];
}

export interface PolygonRunRoute {
  active: boolean;
  runId: string | null;
  error: string | null;
}

export class PolygonRunContractError extends Error {}

export class PolygonRunApiError extends Error {
  constructor(message: string, readonly status: number | null = null) {
    super(message);
  }
}

type PolygonRunFetch = (
  input: RequestInfo | URL,
  init?: RequestInit,
) => Promise<Response>;

const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SHA256 = /^[a-f0-9]{64}$/;
const GIT_REVISION = /^[a-f0-9]{7,64}$/;
const ISO_WITH_TIMEZONE = /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/;
const RUN_STATES = new Set<PolygonRunState>([
  "admitted",
  "starting",
  "running",
  "paused",
  "stopping",
  "completed",
  "failed",
  "aborted",
]);
const REPRODUCIBILITY_TIERS = new Set(["R0", "R1", "R2"]);
const SUMMARY_KEYS = new Set([
  "run_id",
  "episode_id",
  "kind",
  "state",
  "created_at_utc",
  "started_at_utc",
  "ended_at_utc",
  "terminal_reason",
  "scenario_generation",
  "profile_generation",
  "mission_core_commit",
  "host_profile_id",
  "reproducibility_tier",
  "clock_domain",
  "revision",
  "provider_ids",
  "artifact_count",
]);
const RUN_KEYS = new Set([
  ...SUMMARY_KEYS,
  "scenario_sha256",
  "profile_sha256",
  "host_profile_sha256",
  "seed",
  "parent_run_id",
  "providers",
  "authority",
]);
const PROVIDER_KEYS = new Set(["identifier", "version", "revision", "digest"]);
const AUTHORITY_KEYS = new Set([
  "generation",
  "command_ttl_max_ns",
  "heartbeat_timeout_monotonic_ns",
  "simulation_or_shadow_only",
  "actuator_authority",
  "navigation_or_safety_accepted",
  "direct_actuator_setpoints_allowed",
]);
const EVENT_KEYS = new Set([
  "schema_version",
  "run_id",
  "sequence",
  "event_type",
  "observed_at_utc",
  "host_monotonic_ns",
  "sim_time_ns",
  "payload",
]);
const ARTIFACT_KEYS = new Set([
  "artifact_id",
  "kind",
  "relative_path",
  "sha256",
  "byte_length",
  "source_of_record",
  "sequence",
]);
const CATALOG_KEYS = new Set([
  "schema_version",
  "access",
  "items",
  "total",
  "limitations",
]);
const DETAIL_KEYS = new Set([
  "schema_version",
  "access",
  "run",
  "events",
  "events_total",
  "events_truncated",
  "commands",
  "artifacts",
  "limitations",
]);
const COMMAND_KEYS = new Set(["count", "content_exposed"]);

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function assertExactKeys(
  value: Record<string, unknown>,
  expected: ReadonlySet<string>,
  label: string,
) {
  const keys = Object.keys(value);
  if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
    throw new PolygonRunContractError(`${label} содержит неизвестные или отсутствующие поля.`);
  }
}

function requireString(value: unknown, field: string, maximum = 512): string {
  if (typeof value !== "string") {
    throw new PolygonRunContractError(`Поле ${field} должно быть строкой.`);
  }
  const normalized = value.trim();
  if (!normalized || normalized.length > maximum) {
    throw new PolygonRunContractError(`Поле ${field} имеет недопустимую длину.`);
  }
  return normalized;
}

function requireId(value: unknown, field: string): string {
  const identifier = requireString(value, field, 128);
  if (!SAFE_ID.test(identifier)) {
    throw new PolygonRunContractError(`Поле ${field} содержит небезопасный идентификатор.`);
  }
  return identifier;
}

function requireInteger(value: unknown, field: string, minimum = 0): number {
  if (
    typeof value !== "number" ||
    !Number.isSafeInteger(value) ||
    value < minimum
  ) {
    throw new PolygonRunContractError(`Поле ${field} должно быть безопасным целым числом.`);
  }
  return value;
}

function requireTimestamp(value: unknown, field: string): string {
  const timestamp = requireString(value, field, 64);
  if (!ISO_WITH_TIMEZONE.test(timestamp) || !Number.isFinite(Date.parse(timestamp))) {
    throw new PolygonRunContractError(`Поле ${field} должно быть ISO-датой с часовым поясом.`);
  }
  return timestamp;
}

function optionalTimestamp(value: unknown, field: string): string | null {
  return value === null ? null : requireTimestamp(value, field);
}

function optionalString(value: unknown, field: string, maximum = 512): string | null {
  return value === null ? null : requireString(value, field, maximum);
}

function requireBoolean(value: unknown, field: string): boolean {
  if (typeof value !== "boolean") {
    throw new PolygonRunContractError(`Поле ${field} должно быть boolean.`);
  }
  return value;
}

function requireSha256(value: unknown, field: string): string {
  const digest = requireString(value, field, 64);
  if (!SHA256.test(digest)) {
    throw new PolygonRunContractError(`Поле ${field} должно содержать SHA-256.`);
  }
  return digest;
}

function decodeLimitations(value: unknown): string[] {
  if (!Array.isArray(value) || value.length > 16) {
    throw new PolygonRunContractError("Ограничения UI-0 должны быть ограниченным массивом.");
  }
  return value.map((entry, index) => requireString(entry, `limitations[${index}]`, 1_000));
}

function decodeSummary(
  value: unknown,
  expectedKeys: ReadonlySet<string> = SUMMARY_KEYS,
): PolygonRunSummary {
  if (!isRecord(value)) {
    throw new PolygonRunContractError("Сводка прогона должна быть объектом.");
  }
  assertExactKeys(value, expectedKeys, "Сводка прогона");
  const stateValue = requireString(value.state, "state", 32);
  if (!RUN_STATES.has(stateValue as PolygonRunState)) {
    throw new PolygonRunContractError("Прогон содержит неизвестное состояние.");
  }
  const tier = requireString(value.reproducibility_tier, "reproducibility_tier", 2);
  if (!REPRODUCIBILITY_TIERS.has(tier)) {
    throw new PolygonRunContractError("Прогон содержит неизвестный уровень воспроизводимости.");
  }
  const commit = requireString(value.mission_core_commit, "mission_core_commit", 64);
  if (!GIT_REVISION.test(commit)) {
    throw new PolygonRunContractError("Прогон не содержит допустимую ревизию Mission Core.");
  }
  if (!Array.isArray(value.provider_ids) || value.provider_ids.length > 32) {
    throw new PolygonRunContractError("Поле provider_ids должно быть ограниченным массивом.");
  }
  const providerIds = value.provider_ids.map((entry, index) =>
    requireId(entry, `provider_ids[${index}]`));
  if (new Set(providerIds).size !== providerIds.length) {
    throw new PolygonRunContractError("Поле provider_ids содержит повторения.");
  }
  const startedAtUtc = optionalTimestamp(value.started_at_utc, "started_at_utc");
  const endedAtUtc = optionalTimestamp(value.ended_at_utc, "ended_at_utc");
  if (startedAtUtc && endedAtUtc && Date.parse(endedAtUtc) < Date.parse(startedAtUtc)) {
    throw new PolygonRunContractError("Прогон завершился раньше времени запуска.");
  }
  return {
    runId: requireId(value.run_id, "run_id"),
    episodeId: requireId(value.episode_id, "episode_id"),
    kind: requireId(value.kind, "kind"),
    state: stateValue as PolygonRunState,
    createdAtUtc: requireTimestamp(value.created_at_utc, "created_at_utc"),
    startedAtUtc,
    endedAtUtc,
    terminalReason: optionalString(value.terminal_reason, "terminal_reason"),
    scenarioGeneration: requireId(value.scenario_generation, "scenario_generation"),
    profileGeneration: requireId(value.profile_generation, "profile_generation"),
    missionCoreCommit: commit,
    hostProfileId: requireId(value.host_profile_id, "host_profile_id"),
    reproducibilityTier: tier as "R0" | "R1" | "R2",
    clockDomain: requireString(value.clock_domain, "clock_domain", 128),
    revision: requireInteger(value.revision, "revision"),
    providerIds,
    artifactCount: requireInteger(value.artifact_count, "artifact_count"),
  };
}

function decodeProvider(value: unknown, index: number): PolygonProviderPin {
  if (!isRecord(value)) {
    throw new PolygonRunContractError(`providers[${index}] должен быть объектом.`);
  }
  assertExactKeys(value, PROVIDER_KEYS, `providers[${index}]`);
  const digest = value.digest === null ? null : requireSha256(value.digest, `providers[${index}].digest`);
  return {
    identifier: requireId(value.identifier, `providers[${index}].identifier`),
    version: requireString(value.version, `providers[${index}].version`, 128),
    revision: requireString(value.revision, `providers[${index}].revision`, 128),
    digest,
  };
}

function decodeAuthority(value: unknown): PolygonAuthority {
  if (!isRecord(value)) {
    throw new PolygonRunContractError("authority должен быть объектом.");
  }
  assertExactKeys(value, AUTHORITY_KEYS, "authority");
  return {
    generation: requireInteger(value.generation, "authority.generation", 1),
    commandTtlMaxNs: requireInteger(value.command_ttl_max_ns, "authority.command_ttl_max_ns", 1),
    heartbeatTimeoutMonotonicNs: requireInteger(
      value.heartbeat_timeout_monotonic_ns,
      "authority.heartbeat_timeout_monotonic_ns",
      1,
    ),
    simulationOrShadowOnly: requireBoolean(
      value.simulation_or_shadow_only,
      "authority.simulation_or_shadow_only",
    ),
    actuatorAuthority: requireBoolean(value.actuator_authority, "authority.actuator_authority"),
    navigationOrSafetyAccepted: requireBoolean(
      value.navigation_or_safety_accepted,
      "authority.navigation_or_safety_accepted",
    ),
    directActuatorSetpointsAllowed: requireBoolean(
      value.direct_actuator_setpoints_allowed,
      "authority.direct_actuator_setpoints_allowed",
    ),
  };
}

function decodeRun(value: unknown): PolygonRun {
  if (!isRecord(value)) {
    throw new PolygonRunContractError("Прогон должен быть объектом.");
  }
  const summary = decodeSummary(value, RUN_KEYS);
  if (!Array.isArray(value.providers) || value.providers.length > 32) {
    throw new PolygonRunContractError("providers должен быть ограниченным массивом.");
  }
  const providers = value.providers.map(decodeProvider);
  if (
    providers.length !== summary.providerIds.length ||
    providers.some((provider, index) => provider.identifier !== summary.providerIds[index])
  ) {
    throw new PolygonRunContractError("Провайдеры прогона не совпадают со сводкой.");
  }
  const parentRunId = value.parent_run_id === null
    ? null
    : requireId(value.parent_run_id, "parent_run_id");
  return {
    ...summary,
    scenarioSha256: requireSha256(value.scenario_sha256, "scenario_sha256"),
    profileSha256: requireSha256(value.profile_sha256, "profile_sha256"),
    hostProfileSha256: requireSha256(value.host_profile_sha256, "host_profile_sha256"),
    seed: requireInteger(value.seed, "seed"),
    parentRunId,
    providers,
    authority: decodeAuthority(value.authority),
  };
}

function decodeEvent(value: unknown, index: number, runId: string): PolygonRunEvent {
  if (!isRecord(value)) {
    throw new PolygonRunContractError(`events[${index}] должен быть объектом.`);
  }
  assertExactKeys(value, EVENT_KEYS, `events[${index}]`);
  if (value.schema_version !== "missioncore.qualification-event/v1") {
    throw new PolygonRunContractError(`events[${index}] содержит неизвестную схему.`);
  }
  if (!isRecord(value.payload)) {
    throw new PolygonRunContractError(`events[${index}].payload должен быть объектом.`);
  }
  const eventRunId = requireId(value.run_id, `events[${index}].run_id`);
  if (eventRunId !== runId) {
    throw new PolygonRunContractError(`events[${index}] относится к другому прогону.`);
  }
  return {
    runId: eventRunId,
    sequence: requireInteger(value.sequence, `events[${index}].sequence`, 1),
    eventType: requireId(value.event_type, `events[${index}].event_type`),
    observedAtUtc: requireTimestamp(value.observed_at_utc, `events[${index}].observed_at_utc`),
    hostMonotonicNs: requireInteger(
      value.host_monotonic_ns,
      `events[${index}].host_monotonic_ns`,
    ),
    simTimeNs: value.sim_time_ns === null
      ? null
      : requireInteger(value.sim_time_ns, `events[${index}].sim_time_ns`),
    payload: value.payload,
  };
}

function decodeArtifact(value: unknown, index: number): PolygonRunArtifact {
  if (!isRecord(value)) {
    throw new PolygonRunContractError(`artifacts[${index}] должен быть объектом.`);
  }
  assertExactKeys(value, ARTIFACT_KEYS, `artifacts[${index}]`);
  const relativePath = requireString(value.relative_path, `artifacts[${index}].relative_path`, 512);
  const segments = relativePath.split("/");
  if (
    relativePath.startsWith("/") ||
    relativePath.includes("\\") ||
    segments.some((segment) => !segment || segment === "." || segment === "..")
  ) {
    throw new PolygonRunContractError(`artifacts[${index}] содержит небезопасный путь.`);
  }
  return {
    artifactId: requireId(value.artifact_id, `artifacts[${index}].artifact_id`),
    kind: requireId(value.kind, `artifacts[${index}].kind`),
    relativePath,
    sha256: requireSha256(value.sha256, `artifacts[${index}].sha256`),
    byteLength: requireInteger(value.byte_length, `artifacts[${index}].byte_length`),
    sourceOfRecord: requireBoolean(
      value.source_of_record,
      `artifacts[${index}].source_of_record`,
    ),
    sequence: requireInteger(value.sequence, `artifacts[${index}].sequence`, 1),
  };
}

export function decodePolygonRunCatalog(payload: unknown): PolygonRunCatalog {
  if (!isRecord(payload)) {
    throw new PolygonRunContractError("Каталог прогонов должен быть объектом.");
  }
  assertExactKeys(payload, CATALOG_KEYS, "Каталог прогонов");
  if (
    payload.schema_version !== "missioncore.polygon-run-catalog/v1" ||
    payload.access !== "read-only"
  ) {
    throw new PolygonRunContractError("Каталог не является поддерживаемым read-only контрактом.");
  }
  if (!Array.isArray(payload.items) || payload.items.length > 100) {
    throw new PolygonRunContractError("Каталог должен содержать ограниченный массив items.");
  }
  const items = payload.items.map((item) => decodeSummary(item));
  if (new Set(items.map(({ runId }) => runId)).size !== items.length) {
    throw new PolygonRunContractError("Каталог содержит повторяющиеся прогоны.");
  }
  const total = requireInteger(payload.total, "total");
  if (total < items.length) {
    throw new PolygonRunContractError("Размер каталога меньше числа возвращённых прогонов.");
  }
  return {
    items,
    total,
    limitations: decodeLimitations(payload.limitations),
  };
}

export function decodePolygonRunDetail(payload: unknown): PolygonRunDetail {
  if (!isRecord(payload)) {
    throw new PolygonRunContractError("Детали прогона должны быть объектом.");
  }
  assertExactKeys(payload, DETAIL_KEYS, "Детали прогона");
  if (
    payload.schema_version !== "missioncore.polygon-run-detail/v1" ||
    payload.access !== "read-only"
  ) {
    throw new PolygonRunContractError("Детали не являются поддерживаемым read-only контрактом.");
  }
  const run = decodeRun(payload.run);
  if (!Array.isArray(payload.events) || payload.events.length > 500) {
    throw new PolygonRunContractError("Журнал событий должен быть ограниченным массивом.");
  }
  const events = payload.events.map((event, index) => decodeEvent(event, index, run.runId));
  for (let index = 1; index < events.length; index += 1) {
    if (events[index].sequence !== events[index - 1].sequence + 1) {
      throw new PolygonRunContractError("Возвращённый фрагмент событий не является непрерывным.");
    }
  }
  const eventsTotal = requireInteger(payload.events_total, "events_total");
  const eventsTruncated = requireBoolean(payload.events_truncated, "events_truncated");
  if (eventsTotal < events.length || eventsTruncated !== (eventsTotal !== events.length)) {
    throw new PolygonRunContractError("Метаданные усечения событий противоречат журналу.");
  }
  if (!isRecord(payload.commands)) {
    throw new PolygonRunContractError("commands должен быть объектом.");
  }
  assertExactKeys(payload.commands, COMMAND_KEYS, "commands");
  if (payload.commands.content_exposed !== false) {
    throw new PolygonRunContractError("UI-0 не принимает содержимое команд.");
  }
  if (!Array.isArray(payload.artifacts) || payload.artifacts.length > 500) {
    throw new PolygonRunContractError("Артефакты должны быть ограниченным массивом.");
  }
  const artifacts = payload.artifacts.map(decodeArtifact);
  return {
    run,
    events,
    eventsTotal,
    eventsTruncated,
    commandCount: requireInteger(payload.commands.count, "commands.count"),
    artifacts,
    limitations: decodeLimitations(payload.limitations),
  };
}

export function resolvePolygonRunRoute(search: string): PolygonRunRoute {
  const parameters = new URLSearchParams(search);
  const workspaceValues = parameters.getAll("workspace");
  if (
    workspaceValues.length !== 1
    || !["polygon-datasets", "polygon-run"].includes(workspaceValues[0])
  ) {
    return { active: false, runId: null, error: null };
  }
  const runValues = parameters.getAll("run");
  if (runValues.length === 0) {
    return { active: true, runId: null, error: null };
  }
  if (runValues.length !== 1 || !SAFE_ID.test(runValues[0])) {
    return {
      active: true,
      runId: null,
      error: "Прямая ссылка содержит некорректный идентификатор прогона.",
    };
  }
  return { active: true, runId: runValues[0], error: null };
}

async function responseBody(response: Response): Promise<unknown> {
  const contentType = response.headers.get("content-type") ?? "";
  if (!contentType.toLowerCase().startsWith("application/json")) {
    throw new PolygonRunContractError("Polygon API вернул ответ не в формате JSON.");
  }
  try {
    return await response.json();
  } catch {
    throw new PolygonRunContractError("Polygon API вернул повреждённый JSON.");
  }
}

function apiErrorMessage(body: unknown, fallback: string): string {
  if (!isRecord(body) || typeof body.detail !== "string") return fallback;
  const detail = body.detail.trim();
  return detail && detail.length <= 1_000 ? detail : fallback;
}

async function getJson(
  url: string,
  fallback: string,
  signal: AbortSignal | undefined,
  fetcher: PolygonRunFetch,
): Promise<unknown> {
  let response: Response;
  try {
    response = await fetcher(url, {
      method: "GET",
      headers: { Accept: "application/json" },
      signal,
    });
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new PolygonRunApiError(fallback);
  }
  const body = await responseBody(response);
  if (!response.ok) {
    throw new PolygonRunApiError(
      apiErrorMessage(body, `${fallback} HTTP ${response.status}.`),
      response.status,
    );
  }
  return body;
}

export async function fetchPolygonRunCatalog({
  signal,
  limit = 20,
  fetcher = globalThis.fetch,
}: {
  signal?: AbortSignal;
  limit?: number;
  fetcher?: PolygonRunFetch;
} = {}): Promise<PolygonRunCatalog> {
  const admittedLimit = Number.isInteger(limit) && limit >= 1 && limit <= 100 ? limit : 20;
  const body = await getJson(
    `/api/v1/polygon/runs?limit=${admittedLimit}`,
    "Не удалось загрузить каталог прогонов Полигона.",
    signal,
    fetcher,
  );
  return decodePolygonRunCatalog(body);
}

export async function fetchPolygonRunDetail(
  runId: string,
  {
    signal,
    eventLimit = 200,
    fetcher = globalThis.fetch,
  }: {
    signal?: AbortSignal;
    eventLimit?: number;
    fetcher?: PolygonRunFetch;
  } = {},
): Promise<PolygonRunDetail> {
  if (!SAFE_ID.test(runId)) {
    throw new PolygonRunContractError("Идентификатор прогона имеет недопустимый формат.");
  }
  const admittedLimit = Number.isInteger(eventLimit) && eventLimit >= 1 && eventLimit <= 500
    ? eventLimit
    : 200;
  const body = await getJson(
    `/api/v1/polygon/runs/${encodeURIComponent(runId)}?event_limit=${admittedLimit}`,
    "Не удалось загрузить доказательства прогона.",
    signal,
    fetcher,
  );
  return decodePolygonRunDetail(body);
}
