#!/usr/bin/env node
import { randomBytes } from "node:crypto";
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";

const require = createRequire(import.meta.url);
const cwdRequire = createRequire(path.join(process.cwd(), "package.json"));
const YAML = globalThis.unideskYaml ?? optionalRequire("yaml") ?? optionalRequire("yaml", cwdRequire);
const VALIDATOR_UNAVAILABLE = "FEATURE_CONFIG_VALIDATOR_UNAVAILABLE";

export const FEATURE_CONFIG_SCHEMA_PATH = "config/feature-config.schema.json";

export function validateFeatureConfigSchema(options = {}) {
  const repoDir = path.resolve(options.repoDir ?? process.cwd());
  const schemaPath = FEATURE_CONFIG_SCHEMA_PATH;
  const absoluteSchemaPath = path.join(repoDir, schemaPath);
  if (!existsSync(absoluteSchemaPath)) return result("feature-config-schema-missing", "schema file is missing", 1, null);

  let schema;
  try {
    schema = JSON.parse(readFileSync(absoluteSchemaPath, "utf8"));
  } catch (error) {
    return result("feature-config-schema-invalid", `schema JSON parse failed: ${errorMessage(error)}`, 1, null);
  }

  const featureErrors = featureDeclarationErrors(schema);
  if (featureErrors.invalid.length > 0) return result("feature-config-schema-invalid", featureErrors.invalid[0], featureErrors.invalid.length, null);
  if (featureErrors.duplicates.length > 0) return result("feature-config-variable-duplicate", featureErrors.duplicates[0], featureErrors.duplicates.length, null);

  let validate;
  try {
    const Ajv2020 = ajv2020();
    const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
    ajv.addKeyword({ keyword: "x-unidesk-feature", schemaType: "string", valid: true });
    validate = ajv.compile(schema);
  } catch (error) {
    return result(
      error?.code === VALIDATOR_UNAVAILABLE ? "feature-config-validator-unavailable" : "feature-config-schema-invalid",
      errorMessage(error),
      1,
      null,
    );
  }

  let candidate;
  try {
    candidate = options.candidate ?? renderedWorkloadCandidate({
      renderedRoot: options.renderedRoot,
      propertyNames: Object.keys(schema.properties),
    });
  } catch (error) {
    return result("feature-config-validator-failure", errorMessage(error), 1, null);
  }
  if (candidate === null || !isRecord(candidate.values) || Object.keys(candidate.values).length === 0) {
    return result("feature-config-input-missing", "rendered workload candidate snapshot is missing", 1, candidate?.summary ?? null);
  }

  if (!validate(candidate.values)) {
    const errors = Array.isArray(validate.errors) ? validate.errors : [];
    return result("feature-config-value-mismatch", boundedAjvError(errors[0]), errors.length || 1, candidate.summary);
  }
  return result("feature-config-schema-valid", null, 0, candidate.summary);
}

export function emitFeatureConfigSchemaValidation(options = {}) {
  const validation = validateFeatureConfigSchema(options);
  process.stderr.write(`${JSON.stringify(validation)}\n`);
  return validation;
}

export async function runFeatureConfigSchemaValidation(options = {}) {
  let validation;
  try {
    validation = emitFeatureConfigSchemaValidation(options);
  } catch (error) {
    validation = result("feature-config-validator-failure", errorMessage(error), 1, null);
    process.stderr.write(`${JSON.stringify(validation)}\n`);
  }
  try {
    const endpoint = options.otelEndpoint ?? process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ?? "";
    const timeoutMs = positiveInteger(options.otelTimeoutMs ?? process.env.OTEL_EXPORTER_TIMEOUT_MS);
    const samplingRatio = samplingRatioValue(options.otelSamplingRatio ?? process.env.OTEL_TRACES_SAMPLER_ARG);
    if (endpoint.length === 0) {
      const exportWarning = otelWarning("ENDPOINT_MISSING", null, false, null, timeoutMs);
      process.stderr.write(`${JSON.stringify(exportWarning)}\n`);
      return { validation, otelPayload: null, otelWarning: exportWarning };
    }
    if (timeoutMs === null) {
      const exportWarning = otelWarning("TIMEOUT_MISSING", null, false, null, null);
      process.stderr.write(`${JSON.stringify(exportWarning)}\n`);
      return { validation, otelPayload: null, otelWarning: exportWarning };
    }
    const otelPayload = featureConfigOtelPayload(validation, {
      serviceName: options.otelServiceName ?? process.env.OTEL_SERVICE_NAME ?? "unidesk-cicd",
      consumer: options.consumer ?? process.env.UNIDESK_PAC_CONSUMER ?? null,
      samplingRatio,
      traceparent: options.traceparent ?? process.env.TRACEPARENT ?? process.env.traceparent ?? "",
      nowMs: options.nowMs,
      traceId: options.traceId,
      spanId: options.spanId,
    });
    const exportWarning = await exportFeatureConfigOtel(endpoint, otelPayload, timeoutMs, options.fetchImpl ?? fetch);
    if (exportWarning !== null) process.stderr.write(`${JSON.stringify(exportWarning)}\n`);
    return { validation, otelPayload, otelWarning: exportWarning };
  } catch (error) {
    const exportWarning = otelWarning("EXPORT_INTERNAL_FAILURE", null, false, error, positiveInteger(options.otelTimeoutMs ?? process.env.OTEL_EXPORTER_TIMEOUT_MS));
    process.stderr.write(`${JSON.stringify(exportWarning)}\n`);
    return { validation, otelPayload: null, otelWarning: exportWarning };
  }
}

export function featureConfigOtelPayload(validation, options = {}) {
  const parent = parseTraceparent(options.traceparent);
  const traceId = options.traceId ?? parent.traceId ?? randomBytes(16).toString("hex");
  const spanId = options.spanId ?? randomBytes(8).toString("hex");
  const nowMs = options.nowMs ?? Date.now();
  const startTimeUnixNano = String(BigInt(nowMs) * 1_000_000n);
  return {
    resourceSpans: [{
      resource: { attributes: otelAttributes({
        "service.name": options.serviceName ?? "unidesk-cicd",
        "unidesk.pac.consumer": options.consumer ?? null,
        "unidesk.otel.sampling_ratio": options.samplingRatio ?? null,
        "unidesk.values_redacted": true,
      }) },
      scopeSpans: [{
        scope: { name: "unidesk.cicd.feature-config", version: "v1" },
        spans: [{
          traceId,
          spanId,
          ...(parent.spanId === null ? {} : { parentSpanId: parent.spanId }),
          name: "cicd.feature_config.schema",
          kind: 1,
          startTimeUnixNano,
          endTimeUnixNano: startTimeUnixNano,
          attributes: otelAttributes({
            "feature_config.warning": validation.warning,
            "feature_config.blocking": false,
            "feature_config.code": validation.code,
            "feature_config.schema_path": validation.schemaPath,
            "feature_config.error_count": validation.errorCount,
          }),
          events: [{
            timeUnixNano: startTimeUnixNano,
            name: "feature-config-schema-validation",
            attributes: otelAttributes({
              "feature_config.warning": validation.warning,
              "feature_config.code": validation.code,
              "feature_config.first_error": validation.firstError,
            }),
          }],
          status: { code: 1 },
        }],
      }],
    }],
  };
}

async function exportFeatureConfigOtel(endpoint, payload, timeoutMs, fetchImpl) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetchImpl(endpoint, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(payload),
      signal: controller.signal,
    });
    if (response.ok) return null;
    return otelWarning(`HTTP_${response.status}`, response.status, false, null, timeoutMs);
  } catch (error) {
    return otelWarning(error instanceof Error && error.name === "AbortError" ? "TIMEOUT" : "EXPORT_FAILED", null, error instanceof Error && error.name === "AbortError", error, timeoutMs);
  } finally {
    clearTimeout(timeout);
  }
}

function featureDeclarationErrors(schema) {
  if (!isRecord(schema) || schema.type !== "object" || !isRecord(schema.properties)) {
    return { invalid: ["schema root must be an object schema with properties"], duplicates: [] };
  }
  const invalid = [];
  const variablesByFeature = new Map();
  for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {
    if (!isRecord(propertySchema)) {
      invalid.push(`property ${propertyName} schema must be an object`);
      continue;
    }
    const feature = propertySchema["x-unidesk-feature"];
    if (typeof feature !== "string" || feature.trim().length === 0) {
      invalid.push(`property ${propertyName} must declare x-unidesk-feature`);
      continue;
    }
    const variables = variablesByFeature.get(feature.trim()) ?? [];
    variables.push(propertyName);
    variablesByFeature.set(feature.trim(), variables);
  }
  const duplicates = [...variablesByFeature.entries()]
    .filter(([, variables]) => variables.length > 1)
    .map(([feature, variables]) => `feature ${feature} maps to multiple properties: ${variables.join(",")}`);
  return { invalid, duplicates };
}

function renderedWorkloadCandidate({ renderedRoot, propertyNames }) {
  if (typeof renderedRoot !== "string" || renderedRoot.length === 0 || !existsSync(renderedRoot) || YAML === null) return null;
  const wanted = new Set(propertyNames);
  const values = {};
  let fileCount = 0;
  let workloadCount = 0;
  let matchedValueCount = 0;
  for (const file of listStructuredFiles(renderedRoot)) {
    fileCount += 1;
    for (const document of parseDocuments(file)) {
      for (const item of kubernetesItems(document)) {
        const podSpec = item?.spec?.template?.spec;
        if (!isRecord(podSpec)) continue;
        workloadCount += 1;
        for (const group of ["containers", "initContainers"]) {
          for (const container of Array.isArray(podSpec[group]) ? podSpec[group] : []) {
            for (const env of Array.isArray(container?.env) ? container.env : []) {
              if (!wanted.has(env?.name) || typeof env?.value !== "string") continue;
              values[env.name] = parseRenderedValue(env.value);
              matchedValueCount += 1;
            }
          }
        }
      }
    }
  }
  return {
    values,
    summary: {
      source: "rendered-workload-env",
      renderedRoot: path.relative(process.cwd(), path.resolve(renderedRoot)) || ".",
      fileCount,
      workloadCount,
      matchedValueCount,
      propertyCount: propertyNames.length,
      valuesPrinted: false,
    },
  };
}

function listStructuredFiles(root) {
  const maxFiles = 2_000;
  const maxDirectories = 2_000;
  const files = [];
  const pending = [path.resolve(root)];
  let directoryCount = 0;
  while (pending.length > 0) {
    const current = pending.pop();
    directoryCount += 1;
    if (directoryCount > maxDirectories) throw new Error(`rendered workload directory limit exceeded: ${maxDirectories}`);
    for (const name of readdirSync(current)) {
      const file = path.join(current, name);
      const stat = lstatSync(file);
      if (stat.isSymbolicLink()) continue;
      if (stat.isDirectory()) pending.push(file);
      else if (/\.(?:ya?ml|json)$/iu.test(name)) {
        files.push(file);
        if (files.length > maxFiles) throw new Error(`rendered workload file limit exceeded: ${maxFiles}`);
      }
    }
  }
  return files.sort();
}

function parseDocuments(file) {
  const text = readFileSync(file, "utf8");
  if (/\.json$/iu.test(file)) {
    try { return [JSON.parse(text)]; } catch { return []; }
  }
  try { return YAML.parseAllDocuments(text).map((document) => document.toJS()).filter((document) => document !== null); } catch { return []; }
}

function kubernetesItems(document) {
  return document?.kind === "List" && Array.isArray(document.items) ? document.items : [document];
}

function parseRenderedValue(value) {
  try { return JSON.parse(value); } catch { return value; }
}

function boundedAjvError(error) {
  if (!isRecord(error)) return "candidate does not match schema";
  const instancePath = typeof error.instancePath === "string" ? error.instancePath : "";
  const keyword = typeof error.keyword === "string" ? error.keyword : "validation";
  const message = typeof error.message === "string" ? error.message : "failed";
  return `${instancePath || "/"} ${keyword} ${message}`;
}

function result(code, firstError, errorCount, candidate) {
  const warning = errorCount > 0;
  const boundedError = typeof firstError === "string" ? firstError.replace(/\s+/gu, " ").trim().slice(0, 240) : null;
  return {
    event: "feature-config-schema-validation",
    warning,
    blocking: false,
    code,
    schemaPath: FEATURE_CONFIG_SCHEMA_PATH,
    errorCount,
    firstError: boundedError,
    candidate,
    mutation: false,
    valuesPrinted: false,
    otel: {
      spanEvent: "cicd.feature_config.schema",
      attributes: {
        "feature_config.warning": warning,
        "feature_config.code": code,
        "feature_config.schema_path": FEATURE_CONFIG_SCHEMA_PATH,
        "feature_config.error_count": errorCount,
        "feature_config.blocking": false,
      },
    },
  };
}

function optionalRequire(name, loader = require) {
  try { return loader(name); } catch { return null; }
}

function ajv2020() {
  if (globalThis.unideskAjv2020) return globalThis.unideskAjv2020;
  if (typeof process.env.UNIDESK_AJV2020_BUNDLE === "string" && process.env.UNIDESK_AJV2020_BUNDLE.length > 0) {
    return bundledAjv(process.env.UNIDESK_AJV2020_BUNDLE);
  }
  try {
    return require("ajv-dist/dist/ajv2020.bundle.js");
  } catch {
    throw validatorUnavailable("Ajv 2020 validator dependency is unavailable");
  }
}

function bundledAjv(file) {
  if (!existsSync(file)) throw validatorUnavailable("Ajv 2020 validator bundle is missing");
  try {
    const module = { exports: {} };
    Function("module", "exports", readFileSync(file, "utf8"))(module, module.exports);
    const exported = module.exports.default ?? module.exports;
    if (typeof exported !== "function") throw new TypeError("bundle export is not a constructor");
    return exported;
  } catch (error) {
    if (error?.code === VALIDATOR_UNAVAILABLE) throw error;
    throw validatorUnavailable(`Ajv 2020 validator bundle is corrupt (${error instanceof Error ? error.name : "Error"})`);
  }
}

function validatorUnavailable(message) {
  const error = new Error(message);
  error.code = VALIDATOR_UNAVAILABLE;
  return error;
}

function isRecord(value) {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function errorMessage(error) {
  return error instanceof Error ? error.message : String(error);
}

function positiveInteger(value) {
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}

function samplingRatioValue(value) {
  const parsed = Number(value);
  return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : null;
}

function parseTraceparent(value) {
  const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$/iu.exec(String(value));
  return { traceId: match?.[1]?.toLowerCase() ?? null, spanId: match?.[2]?.toLowerCase() ?? null };
}

function otelAttributes(values) {
  return Object.entries(values).filter(([, value]) => value !== null && value !== undefined).map(([key, value]) => ({
    key,
    value: typeof value === "boolean"
      ? { boolValue: value }
      : typeof value === "number"
        ? { intValue: String(value) }
        : { stringValue: String(value) },
  }));
}

function otelWarning(causeCode, httpStatus, timedOut, error, timeoutMs) {
  return {
    event: "cicd.otel.export",
    warning: true,
    blocking: false,
    causeCode,
    ...(httpStatus === null ? {} : { httpStatus }),
    timedOut,
    errorType: error instanceof Error ? error.name : null,
    timeoutMs,
    valuesRedacted: true,
  };
}

if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
  const renderedRootIndex = process.argv.indexOf("--rendered-root");
  await runFeatureConfigSchemaValidation({ renderedRoot: renderedRootIndex === -1 ? null : process.argv[renderedRootIndex + 1] });
  process.exitCode = 0;
}
