import { createHash } from "node:crypto";
import { validateProviderPackage } from "./provider-package.mjs";
import {
  compileTelemetryFieldProjection,
  validateTelemetryFieldRegistry,
} from "./telemetry-field-registry.mjs";

export const L2_EXECUTION_PLAN_SCHEMA_VERSION = "nodedc.l2-execution-plan/v1";
export const L2_GRAPH_BLUEPRINT_SCHEMA_VERSION = "nodedc.l2-graph-blueprint/v1";
export const L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION = "nodedc.l2-materialization-receipt/v1";
export const L2_MAPPING_RUNTIME_SCHEMA_VERSION = "nodedc.semantic-mapping-runtime/v1";
export const L2_EXECUTION_PLAN_COMPILER_VERSION = "1.5.0";
export const L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS = Object.freeze([
  "1.1.0",
  "1.2.0",
  "1.3.0",
  "1.4.0",
  L2_EXECUTION_PLAN_COMPILER_VERSION,
]);

const HASH = /^(?:sha256:)?[a-f0-9]{64}$/;
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const CREDENTIAL_REF = /^ndc-credref:[A-Za-z0-9._:-]{8,255}$/;
const COMPILE_OPTION_KEYS = new Set(["telemetryFieldRegistry"]);
const RECEIPT_OPTION_KEYS = new Set(["graphRevision", "graphDigest", "materializedStepIds"]);
const STEP_RUNTIME_KINDS = Object.freeze({
  collection_trigger: "ndc.collection-trigger",
  provider_request: "ndc.provider-request",
  extract_items: "ndc.extract-items",
  semantic_mapping: "ndc.semantic-mapping",
  data_product_publish: "ndc.data-product-publish",
});

/**
 * Resolve one immutable provider package plus one connection instance into a
 * deterministic, provider-neutral execution plan. Provider-specific knowledge
 * stays in package/registry data; compiler branching is only by generic step
 * kind.
 */
export function compileL2ExecutionPlan(providerPackage, connectionInstance, options = {}) {
  const packageValidation = validateProviderPackage(providerPackage);
  if (!packageValidation.ok) {
    throw new Error(`provider_package_invalid:${packageValidation.errors.join(",")}`);
  }
  validateCompileOptions(options);
  validateConnectionInstance(providerPackage, connectionInstance);

  const profile = requireById(
    providerPackage.collectionProfiles,
    connectionInstance.collectionProfileId,
    "collection_profile",
  );
  const template = requireById(providerPackage.l2Templates, profile.l2TemplateId, "l2_template");
  if (template.id !== connectionInstance.l2TemplateId) {
    throw new Error("l2_execution_plan_connection_template_mismatch");
  }
  const mapping = requireById(providerPackage.mappingContracts, profile.mappingContractId, "mapping_contract");
  const fieldPolicy = requireById(providerPackage.fieldPolicies, profile.fieldPolicyId, "field_policy");
  const dataProduct = requireById(providerPackage.dataProducts, profile.dataProductId, "data_product");
  const requiresTelemetryRegistry = Object.values(dataProduct.fieldContracts || {})
    .some((contract) => contract?.type === "telemetry_readings");

  let telemetryProjection;
  if (options.telemetryFieldRegistry !== undefined) {
    const registryValidation = validateTelemetryFieldRegistry(options.telemetryFieldRegistry);
    if (!registryValidation.ok) {
      throw new Error(`telemetry_field_registry_invalid:${registryValidation.errors.join(",")}`);
    }
    telemetryProjection = compileTelemetryFieldProjection(options.telemetryFieldRegistry, {
      providerPackageId: providerPackage.id,
      providerPackageVersion: providerPackage.version,
      dataProductId: dataProduct.id,
      surface: "data_product",
    });
  } else if (requiresTelemetryRegistry) {
    throw new Error("l2_execution_plan_telemetry_registry_required");
  }
  if (requiresTelemetryRegistry && telemetryProjection.entries.length === 0) {
    throw new Error("l2_execution_plan_telemetry_projection_empty");
  }
  if (!requiresTelemetryRegistry && telemetryProjection !== undefined) {
    throw new Error("l2_execution_plan_telemetry_registry_not_applicable");
  }

  const artifacts = {
    packageDigest: canonicalDigest(providerPackage),
    collectionProfileDigest: canonicalDigest(profile),
    l2TemplateDigest: canonicalDigest(template),
    mappingContractDigest: canonicalDigest(mapping),
    fieldPolicyDigest: canonicalDigest(fieldPolicy),
    dataProductDigest: canonicalDigest(dataProduct),
    ...(telemetryProjection ? {
      telemetryRegistryDigest: telemetryProjection.registry.digest,
      telemetryProjectionDigest: canonicalDigest(telemetryProjection),
    } : {}),
  };

  const steps = template.steps.map((step) => compileStep({
    step,
    providerPackage,
    profile,
    mapping,
    fieldPolicy,
    dataProduct,
    telemetryProjection,
    artifacts,
  }));
  const graphBlueprint = compileGraphBlueprint({
    profile,
    template,
    mapping,
    steps,
  });

  const planWithoutDigest = {
    schemaVersion: L2_EXECUTION_PLAN_SCHEMA_VERSION,
    compilerVersion: L2_EXECUTION_PLAN_COMPILER_VERSION,
    package: {
      id: providerPackage.id,
      providerId: providerPackage.providerId,
      version: providerPackage.version,
    },
    connection: {
      tenantId: connectionInstance.tenantId,
      connectionId: connectionInstance.connectionId,
      collectionProfileId: profile.id,
      l2TemplateId: template.id,
    },
    bindings: {
      provider: structuredClone(connectionInstance.credentialRefs.provider),
      publisher: structuredClone(connectionInstance.systemBindings.publisher),
    },
    collectionProfile: structuredClone(profile),
    artifacts,
    steps,
    graphBlueprint,
  };

  return deepFreeze({
    ...planWithoutDigest,
    executionPlanDigest: canonicalDigest(planWithoutDigest),
  });
}

export function validateL2ExecutionPlan(value) {
  const errors = [];
  if (!isPlainObject(value)) return result(["executionPlan_must_be_object"]);
  if (value.schemaVersion !== L2_EXECUTION_PLAN_SCHEMA_VERSION) errors.push("executionPlan.schemaVersion_mismatch");
  if (!L2_EXECUTION_PLAN_SUPPORTED_COMPILER_VERSIONS.includes(value.compilerVersion)) {
    errors.push("executionPlan.compilerVersion_mismatch");
  }
  if (!Array.isArray(value.steps) || value.steps.length < 5) {
    errors.push("executionPlan.steps_invalid");
  } else {
    const ids = value.steps.map((step) => step?.id);
    if (ids.some((id) => typeof id !== "string" || !IDENTIFIER.test(id))) errors.push("executionPlan.steps_id_invalid");
    if (new Set(ids).size !== ids.length) errors.push("executionPlan.steps_id_must_be_unique");
  }
  validateGraphBlueprint(value, errors);
  if (!HASH.test(String(value.executionPlanDigest || ""))) {
    errors.push("executionPlan.executionPlanDigest_invalid");
  } else {
    const descriptor = structuredClone(value);
    delete descriptor.executionPlanDigest;
    if (normalizeDigest(value.executionPlanDigest) !== canonicalDigest(descriptor)) {
      errors.push("executionPlan.executionPlanDigest_mismatch");
    }
  }
  return result(errors);
}

/**
 * Produce the exact provider-neutral graph shape required by one resolved
 * execution plan. Runtime adapters may translate these generic node kinds to
 * their native graph representation, but may not add provider branches or
 * silently weaken the declared execution policies.
 */
export function compileL2GraphBlueprint(executionPlan) {
  const validation = validateL2ExecutionPlan(executionPlan);
  if (!validation.ok) {
    throw new Error(`l2_execution_plan_invalid:${validation.errors.join(",")}`);
  }
  return executionPlan.graphBlueprint;
}

/**
 * Bind a successful Engine graph materialization back to the exact compiled
 * plan without teaching this package how Engine stores or renders its graph.
 */
export function attestL2ExecutionPlanMaterialization(executionPlan, options) {
  const validation = validateL2ExecutionPlan(executionPlan);
  if (!validation.ok) {
    throw new Error(`l2_execution_plan_invalid:${validation.errors.join(",")}`);
  }
  if (!isPlainObject(options)) throw new Error("l2_materialization_options_must_be_object");
  const errors = [];
  rejectUnknownKeys(options, RECEIPT_OPTION_KEYS, "options", errors);
  if (typeof options.graphRevision !== "string" || !options.graphRevision.trim() || options.graphRevision.length > 160) {
    errors.push("options.graphRevision_invalid");
  }
  if (!HASH.test(String(options.graphDigest || ""))) errors.push("options.graphDigest_invalid");
  const expectedStepIds = executionPlan.steps.map((step) => step.id);
  if (!Array.isArray(options.materializedStepIds)
    || options.materializedStepIds.length !== expectedStepIds.length
    || options.materializedStepIds.some((id, index) => id !== expectedStepIds[index])) {
    errors.push("options.materializedStepIds_must_exactly_match_execution_plan");
  }
  if (errors.length) {
    throw new Error(`l2_materialization_invalid:${[...new Set(errors)].join(",")}`);
  }

  const receiptWithoutDigest = {
    schemaVersion: L2_MATERIALIZATION_RECEIPT_SCHEMA_VERSION,
    compilerVersion: executionPlan.compilerVersion,
    executionPlanDigest: executionPlan.executionPlanDigest,
    graphRevision: options.graphRevision,
    graphDigest: normalizeDigest(options.graphDigest),
    materializedStepIds: [...options.materializedStepIds],
  };
  return deepFreeze({
    ...receiptWithoutDigest,
    receiptDigest: canonicalDigest(receiptWithoutDigest),
  });
}

function compileStep(context) {
  const { step, providerPackage, profile, mapping, fieldPolicy, dataProduct, telemetryProjection, artifacts } = context;
  const base = { id: step.id, kind: step.kind };
  if (step.kind === "collection_trigger") {
    return {
      ...base,
      config: {
        mode: profile.mode,
        ...(profile.schedule ? { schedule: structuredClone(profile.schedule) } : {}),
      },
    };
  }
  if (step.kind === "provider_request") {
    const capability = requireById(providerPackage.capabilities, step.capabilityId, "capability");
    return {
      ...base,
      config: {
        capabilityId: capability.id,
        capabilityDigest: canonicalDigest(capability),
        authModeId: capability.authModeId,
        credentialBinding: "provider",
        request: structuredClone(capability.request),
      },
    };
  }
  if (step.kind === "extract_items") {
    const capability = requireById(providerPackage.capabilities, step.capabilityId, "capability");
    return {
      ...base,
      config: {
        capabilityId: capability.id,
        capabilityDigest: canonicalDigest(capability),
        response: structuredClone(capability.request.response),
      },
    };
  }
  if (step.kind === "semantic_mapping") {
    return {
      ...base,
      config: {
        mappingContractId: mapping.id,
        mappingContractDigest: artifacts.mappingContractDigest,
        fieldPolicyId: fieldPolicy.id,
        fieldPolicyDigest: artifacts.fieldPolicyDigest,
        dataProductId: dataProduct.id,
        dataProductDigest: artifacts.dataProductDigest,
        mappingContract: structuredClone(mapping),
        ...(telemetryProjection ? { telemetryProjection: structuredClone(telemetryProjection) } : {}),
      },
    };
  }
  if (step.kind === "data_product_publish") {
    return {
      ...base,
      config: {
        dataProductId: dataProduct.id,
        dataProductDigest: artifacts.dataProductDigest,
        nodeType: step.nodeType,
        credentialBinding: "publisher",
      },
    };
  }
  throw new Error(`l2_execution_plan_step_kind_unsupported:${String(step.kind)}`);
}

function compileGraphBlueprint({ profile, template, mapping, steps }) {
  const nodes = steps.flatMap((step) => {
    if (step.kind !== "collection_trigger") return [compileBlueprintNode(step, step.id)];
    if (profile.mode !== "realtime") {
      return [compileBlueprintNode(step, step.id, "ndc.manual-trigger", { entrypoint: "manual" })];
    }
    return [
      compileBlueprintNode(
        step,
        `${step.id}.manual`,
        "ndc.manual-trigger",
        { entrypoint: "manual" },
      ),
      compileBlueprintNode(
        step,
        `${step.id}.interval`,
        "ndc.interval-trigger",
        { entrypoint: "interval", intervalMs: profile.schedule.intervalMs },
      ),
    ];
  }).map((node, order) => ({ ...node, order }));
  const triggerNodes = nodes.filter((node) => node.stepId === steps[0].id);
  const executableNodes = nodes.filter((node) => node.stepId !== steps[0].id);
  const edgePairs = [
    ...triggerNodes.map((node) => ({ from: node.id, to: executableNodes[0].id })),
    ...executableNodes.slice(1).map((node, index) => ({
      from: executableNodes[index].id,
      to: node.id,
    })),
  ];
  const edges = edgePairs.map((edge, index) => ({
    id: `edge.${String(index + 1).padStart(3, "0")}`,
    ...edge,
    channel: "main",
    order: 0,
  }));
  const blueprintWithoutDigest = {
    schemaVersion: L2_GRAPH_BLUEPRINT_SCHEMA_VERSION,
    runtime: {
      target: template.runtime,
      instanceMode: template.instanceMode,
      executionMode: profile.mode,
      entrypoints: profile.mode === "realtime"
        ? [
            { kind: "manual", purpose: "managed_deploy_and_run" },
            { kind: "interval", intervalMs: profile.schedule.intervalMs, purpose: "scheduled_collection" },
          ]
        : [{ kind: "manual", purpose: "managed_deploy_and_run" }],
      envelope: {
        schemaVersion: "nodedc.l2-runtime-envelope/v1",
        providerResponses: "keyed_by_capability_id",
        collectionReceivedAt: "required_iso_timestamp",
        canonicalFactsOnlyAtPublish: true,
        rawProviderPayloadAtPublish: "forbidden",
        maxProviderResponseBytes: 16 * 1024 * 1024,
        maxExtractedItems: profile.cardinality.maxCurrentEntities,
      },
      retry: structuredClone(profile.retry),
      batching: structuredClone(profile.batching),
      cardinality: structuredClone(profile.cardinality),
      failureMode: "fail_closed",
      mappingRuntime: compileMappingRuntimeRequirements(mapping),
    },
    nodes,
    edges,
  };
  return {
    ...blueprintWithoutDigest,
    blueprintDigest: canonicalDigest(blueprintWithoutDigest),
  };
}

function compileBlueprintNode(step, id, runtimeKind = STEP_RUNTIME_KINDS[step.kind], adapterConfig) {
  return {
    id,
    stepId: step.id,
    runtimeKind,
    configDigest: canonicalDigest({
      stepConfig: step.config,
      ...(adapterConfig ? { adapterConfig } : {}),
    }),
    ...(adapterConfig ? { adapterConfig } : {}),
    credentialSlots: step.config?.credentialBinding
      ? [{
          role: step.config.credentialBinding,
          slot: step.config.credentialBinding,
          authority: "engine_managed_opaque_ref",
        }]
      : [],
  };
}

function compileMappingRuntimeRequirements(mapping) {
  const expressions = [];
  collectMappingExpressions(mapping.fact, expressions);
  const geometry = mapping.fact?.geometry;
  return {
    schemaVersion: L2_MAPPING_RUNTIME_SCHEMA_VERSION,
    expressionSources: uniqueSorted(expressions.map(expressionSourceKind)),
    coercions: uniqueSorted(expressions.map((expression) => expression.coerce).filter(Boolean)),
    derivationKinds: uniqueSorted(Object.values(mapping.derivations || {}).map((item) => item.kind)),
    ruleIds: uniqueSorted(
      Object.values(mapping.derivations || {}).flatMap((item) => item.rules || []),
    ),
    geometryStrategies: geometry
      ? [geometry.type === "Point" ? "point_from_longitude_latitude" : geometry.strategy]
      : [],
  };
}

function collectMappingExpressions(value, found) {
  if (Array.isArray(value)) {
    value.forEach((item) => collectMappingExpressions(item, found));
    return;
  }
  if (!isPlainObject(value)) return;
  if (Array.isArray(value.paths) || value.constant !== undefined || typeof value.derive === "string") {
    found.push(value);
  }
  Object.values(value).forEach((item) => collectMappingExpressions(item, found));
}

function expressionSourceKind(value) {
  if (Array.isArray(value.paths)) return value.strategy || "first_non_empty";
  if (value.constant !== undefined) return "constant";
  return "derive";
}

function validateGraphBlueprint(executionPlan, errors) {
  if (!isPlainObject(executionPlan.graphBlueprint)) {
    errors.push("executionPlan.graphBlueprint_must_be_object");
    return;
  }
  const blueprint = executionPlan.graphBlueprint;
  if (blueprint.schemaVersion !== L2_GRAPH_BLUEPRINT_SCHEMA_VERSION) {
    errors.push("executionPlan.graphBlueprint.schemaVersion_mismatch");
  }
  if (!HASH.test(String(blueprint.blueprintDigest || ""))) {
    errors.push("executionPlan.graphBlueprint.blueprintDigest_invalid");
  } else {
    const descriptor = structuredClone(blueprint);
    delete descriptor.blueprintDigest;
    if (normalizeDigest(blueprint.blueprintDigest) !== canonicalDigest(descriptor)) {
      errors.push("executionPlan.graphBlueprint.blueprintDigest_mismatch");
    }
  }
  if (!Array.isArray(executionPlan.steps) || !isPlainObject(executionPlan.collectionProfile)) return;
  try {
    const expected = compileGraphBlueprintDescriptor({
      profile: executionPlan.collectionProfile,
      steps: executionPlan.steps,
      mapping: executionPlan.steps.find((step) => step?.kind === "semantic_mapping")?.config?.mappingContract,
    });
    if (stableJson(blueprint) !== stableJson(expected)) {
      errors.push("executionPlan.graphBlueprint_mismatch");
    }
  } catch {
    errors.push("executionPlan.graphBlueprint_unverifiable");
  }
}

function compileGraphBlueprintDescriptor({ profile, steps, mapping }) {
  if (!isPlainObject(mapping)) throw new Error("graph_blueprint_context_invalid");
  const template = {
    runtime: "ndc_l2",
    instanceMode: "one_connection_per_workflow",
  };
  return compileGraphBlueprint({ profile, template, mapping, steps });
}

function uniqueSorted(values) {
  return [...new Set(values)].sort();
}

function validateConnectionInstance(providerPackage, value) {
  if (!isPlainObject(value)) throw new Error("l2_execution_plan_connection_must_be_object");
  if (value.schemaVersion !== "nodedc.l2-connection-instance/v1") {
    throw new Error("l2_execution_plan_connection_schema_mismatch");
  }
  if (value.package?.id !== providerPackage.id
    || value.package?.providerId !== providerPackage.providerId
    || value.package?.version !== providerPackage.version) {
    throw new Error("l2_execution_plan_connection_package_mismatch");
  }
  if (typeof value.tenantId !== "string" || !IDENTIFIER.test(value.tenantId)) {
    throw new Error("l2_execution_plan_tenant_id_invalid");
  }
  if (typeof value.connectionId !== "string" || !IDENTIFIER.test(value.connectionId)) {
    throw new Error("l2_execution_plan_connection_id_invalid");
  }
  if (!CREDENTIAL_REF.test(String(value.credentialRefs?.provider?.reference || ""))
    || value.credentialRefs?.provider?.owner !== "ndc_l2_credentials") {
    throw new Error("l2_execution_plan_provider_credential_ref_invalid");
  }
  const publisher = value.systemBindings?.publisher;
  if (publisher?.owner !== "ndc_l2_credentials"
    || publisher?.management !== "control_plane_managed"
    || publisher?.desiredState !== "bound"
    || publisher?.status !== "unresolved") {
    throw new Error("l2_execution_plan_publisher_binding_invalid");
  }
}

function validateCompileOptions(options) {
  if (!isPlainObject(options)) throw new Error("l2_execution_plan_options_must_be_object");
  const errors = [];
  rejectUnknownKeys(options, COMPILE_OPTION_KEYS, "options", errors);
  if (errors.length) throw new Error(`l2_execution_plan_options_invalid:${errors.join(",")}`);
}

function requireById(items, id, type) {
  const value = Array.isArray(items) ? items.find((item) => item?.id === id) : undefined;
  if (!value) throw new Error(`l2_execution_plan_${type}_missing:${String(id)}`);
  return value;
}

function canonicalDigest(value) {
  return `sha256:${createHash("sha256").update(stableJson(value), "utf8").digest("hex")}`;
}

function normalizeDigest(value) {
  const text = String(value);
  return text.startsWith("sha256:") ? text : `sha256:${text}`;
}

function stableJson(value) {
  return JSON.stringify(stableValue(value));
}

function stableValue(value) {
  if (Array.isArray(value)) return value.map(stableValue);
  if (!isPlainObject(value)) return value;
  return Object.fromEntries(
    Object.keys(value)
      .filter((key) => value[key] !== undefined)
      .sort()
      .map((key) => [key, stableValue(value[key])]),
  );
}

function rejectUnknownKeys(value, allowed, path, errors) {
  for (const key of Object.keys(value)) {
    if (!allowed.has(key)) errors.push(`${path}.${key}_not_allowed`);
  }
}

function result(errors) {
  return { ok: errors.length === 0, errors: [...new Set(errors)] };
}

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

function deepFreeze(input) {
  if (!input || typeof input !== "object" || Object.isFrozen(input)) return input;
  Object.freeze(input);
  for (const child of Object.values(input)) deepFreeze(child);
  return input;
}
