import { rootPath } from "../../src/config";
import { readYamlRecord } from "../../src/platform-infra-ops-library";
import { materializeYamlComposition } from "../../src/yaml-composition";

const pacConfigPath = "config/platform-infra/pipelines-as-code.yaml";

export interface CicdOtelRuntimeConfig {
  readonly endpoint: string;
  readonly endpointHostname: string;
  readonly serviceName: string;
  readonly samplingRatio: number;
  readonly timeoutMs: number;
  readonly fallbackCodes: readonly string[];
}

export interface CicdOtelExportWarning {
  readonly event: "cicd.otel.export";
  readonly warning: true;
  readonly blocking: false;
  readonly causeCode: string;
  readonly endpointHostname: string;
  readonly httpStatus?: number;
  readonly timedOut?: boolean;
  readonly errorType?: string;
  readonly timeoutMs: number;
  readonly traceId: string;
  readonly valuesRedacted: true;
}

export function loadCicdOtelRuntimeConfig(
  repositoryId: string,
  env: Record<string, string | undefined> = process.env,
): CicdOtelRuntimeConfig {
  const root = materializeYamlComposition(
    readYamlRecord<Record<string, unknown>>(rootPath(pacConfigPath), "platform-infra-pipelines-as-code"),
    { label: "platform-infra-pipelines-as-code", stripTemplateKeys: true },
  ).value;
  const observability = record(root.observability, `${pacConfigPath}#observability`);
  const repository = selectCicdOtelRepository(root.repositories, repositoryId);
  const params = record(repository.params, `${pacConfigPath}#repositories.${repository.id}.params`);
  const fallbackCodes: string[] = [];
  const endpoint = authoritativeValue(
    [
      [params.otel_traces_endpoint, "INVALID_ENDPOINT_PARAM"],
      [observability.tracesEndpoint, "INVALID_ENDPOINT_AUTHORITY"],
    ],
    validEndpoint,
    fallbackCodes,
  );
  validateInput(env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, endpoint, validEndpoint, "ENDPOINT", fallbackCodes);
  const endpointUrl = new URL(endpoint);
  const serviceName = authoritativeValue(
    [
      [params.otel_service_name, "INVALID_SERVICE_PARAM"],
      [observability.serviceName, "INVALID_SERVICE_AUTHORITY"],
    ],
    validServiceName,
    fallbackCodes,
  );
  validateInput(env.OTEL_SERVICE_NAME, serviceName, validServiceName, "SERVICE", fallbackCodes);
  const sampling = record(observability.sampling, `${pacConfigPath}#observability.sampling`);
  const samplingRatio = authoritativeNumber(
    [
      [params.otel_sampling_ratio, "INVALID_SAMPLING_PARAM"],
      [sampling.ratio, "INVALID_SAMPLING_AUTHORITY"],
    ],
    (value) => value >= 0 && value <= 1,
    fallbackCodes,
  );
  validateNumberInput(env.OTEL_TRACES_SAMPLER_ARG, samplingRatio, "SAMPLING", fallbackCodes);
  const exporterFailure = record(observability.exporterFailure, `${pacConfigPath}#observability.exporterFailure`);
  const timeoutMs = authoritativeNumber(
    [
      [params.otel_exporter_timeout_ms, "INVALID_TIMEOUT_PARAM"],
      [exporterFailure.timeoutMs, "INVALID_TIMEOUT_AUTHORITY"],
    ],
    (value) => Number.isInteger(value) && value >= 50 && value <= 2_000,
    fallbackCodes,
  );
  validateNumberInput(env.OTEL_EXPORTER_TIMEOUT_MS, timeoutMs, "TIMEOUT", fallbackCodes);
  return {
    endpoint: endpointUrl.toString(),
    endpointHostname: endpointUrl.hostname,
    serviceName,
    samplingRatio,
    timeoutMs,
    fallbackCodes,
  };
}

export async function exportCicdOtelPayload(
  config: CicdOtelRuntimeConfig,
  traceparent: string,
  traceId: string,
  payload: unknown,
  fetcher: typeof fetch = fetch,
): Promise<CicdOtelExportWarning | null> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), config.timeoutMs);
  try {
    const response = await fetcher(config.endpoint, {
      method: "POST",
      headers: { "content-type": "application/json", traceparent },
      body: JSON.stringify(payload),
      signal: controller.signal,
    });
    if (response.ok) return null;
    return warning(config, traceId, `HTTP_${response.status}`, { httpStatus: response.status });
  } catch (error) {
    return warning(config, traceId, errorCauseCode(error), {
      timedOut: error instanceof Error && error.name === "AbortError",
      errorType: error instanceof Error ? error.name : "Error",
    });
  } finally {
    clearTimeout(timer);
  }
}

export function selectCicdOtelRepository(value: unknown, repositoryId: string): Record<string, unknown> {
  if (!Array.isArray(value)) throw new Error(`${pacConfigPath}#repositories must be an array`);
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(repositoryId)) throw new Error(`invalid PaC repository id ${repositoryId}`);
  const repositories = value.filter((item) => recordOrNull(item)?.id === repositoryId);
  if (repositories.length !== 1) throw new Error(`${pacConfigPath} requires exactly one repository ${repositoryId}; matched ${repositories.length}`);
  return record(repositories[0], `${pacConfigPath}#repositories.${repositoryId}`);
}

function authoritativeValue(
  candidates: ReadonlyArray<readonly [unknown, string]>,
  validate: (value: string) => boolean,
  fallbackCodes: string[],
): string {
  for (const [candidate, code] of candidates) {
    if (candidate === undefined || candidate === null || candidate === "") {
      fallbackCodes.push(code);
      continue;
    }
    if (typeof candidate === "string" && validate(candidate)) return candidate;
    fallbackCodes.push(code);
  }
  throw new Error(`invalid owning OTel configuration: ${fallbackCodes.at(-1) ?? "UNKNOWN"}`);
}

function authoritativeNumber(
  candidates: ReadonlyArray<readonly [unknown, string]>,
  validate: (value: number) => boolean,
  fallbackCodes: string[],
): number {
  for (const [candidate, code] of candidates) {
    if (candidate === undefined || candidate === null || candidate === "") {
      fallbackCodes.push(code);
      continue;
    }
    const value = typeof candidate === "number" ? candidate : Number(candidate);
    if (Number.isFinite(value) && validate(value)) return value;
    fallbackCodes.push(code);
  }
  throw new Error(`invalid owning OTel configuration: ${fallbackCodes.at(-1) ?? "UNKNOWN"}`);
}

function validateInput(
  input: string | undefined,
  desired: string,
  validate: (value: string) => boolean,
  label: string,
  fallbackCodes: string[],
): void {
  if (input === undefined || input === "") return;
  if (!validate(input)) fallbackCodes.push(`INVALID_${label}_INPUT`);
  else if (input !== desired) fallbackCodes.push(`MISMATCH_${label}_INPUT`);
}

function validateNumberInput(input: string | undefined, desired: number, label: string, fallbackCodes: string[]): void {
  if (input === undefined || input === "") return;
  const value = Number(input);
  if (!Number.isFinite(value)) fallbackCodes.push(`INVALID_${label}_INPUT`);
  else if (input !== String(desired)) fallbackCodes.push(`MISMATCH_${label}_INPUT`);
}

function validEndpoint(value: string): boolean {
  if (hasTemplatePlaceholder(value)) return false;
  try {
    const url = new URL(value);
    return (url.protocol === "http:" || url.protocol === "https:")
      && url.hostname.length > 0
      && url.username.length === 0
      && url.password.length === 0;
  } catch {
    return false;
  }
}

function validServiceName(value: string): boolean {
  return !hasTemplatePlaceholder(value) && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
}

function hasTemplatePlaceholder(value: string): boolean {
  return /\{\{[^}]+\}\}|\$\{[^}]+\}/u.test(value);
}

function warning(
  config: CicdOtelRuntimeConfig,
  traceId: string,
  causeCode: string,
  details: Pick<CicdOtelExportWarning, "httpStatus" | "timedOut" | "errorType">,
): CicdOtelExportWarning {
  return {
    event: "cicd.otel.export",
    warning: true,
    blocking: false,
    causeCode,
    endpointHostname: config.endpointHostname,
    ...details,
    timeoutMs: config.timeoutMs,
    traceId,
    valuesRedacted: true,
  };
}

function errorCauseCode(error: unknown): string {
  if (error instanceof Error && error.name === "AbortError") return "EXPORT_TIMEOUT";
  const cause = error instanceof Error && typeof error.cause === "object" && error.cause !== null ? error.cause as Record<string, unknown> : null;
  return typeof cause?.code === "string" && /^[A-Za-z0-9_.-]{1,80}$/u.test(cause.code) ? cause.code : "EXPORT_FAILED";
}

function record(value: unknown, path: string): Record<string, unknown> {
  const result = recordOrNull(value);
  if (result === null) throw new Error(`${path} must be an object`);
  return result;
}

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