const IDENTIFIER = /^[a-z][a-z0-9._:-]{1,159}$/;
const FIELD = /^[a-z][a-z0-9_.-]{0,127}$/;
const SEMVER = /^\d+\.\d+\.\d+$/;
const PROFILE_KEYS = new Set([
  "id", "version", "title", "semanticTypes", "label", "target", "pin", "facets",
  "styles", "classes", "defaultClassId", "sort",
]);
const LABEL_KEYS = new Set([
  "mode", "fields", "fontWeight", "sizePx", "color", "outlineColor", "outlineWidthPx",
  "backgroundColor", "backgroundOpacity", "paddingX", "paddingY", "maxLength",
  "offsetX", "offsetY", "hideCameraHeightMeters",
]);
const TARGET_KEYS = new Set([
  "variant", "stemHeightMeters", "headSizePx", "stemWidthPx", "outlineColor",
  "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters",
]);
const FACET_KEYS = new Set(["id", "field", "label", "filterable", "counter", "values"]);
const FACET_VALUE_KEYS = new Set(["value", "label", "order"]);
const STYLE_KEYS = new Set(["id", "color", "opacity"]);
const CLASS_KEYS = new Set(["id", "label", "priority", "match", "styleId", "renderable"]);
const MATCH_KEYS = new Set(["field", "equals"]);
const SORT_KEYS = new Set(["field", "order"]);

export function normalizeMapPresentationProfile(value) {
  object(value, "invalid_map_presentation_profile");
  onlyKeys(value, PROFILE_KEYS, "invalid_map_presentation_profile_fields");
  const id = identifier(value.id, "invalid_map_presentation_profile_id");
  const version = text(value.version, 32, "invalid_map_presentation_profile_version");
  if (!SEMVER.test(version)) fail("invalid_map_presentation_profile_version");
  const title = text(value.title, 120, "invalid_map_presentation_profile_title");
  const semanticTypes = identifiers(value.semanticTypes, 1, 8, "invalid_map_presentation_profile_semantic_types");

  const label = normalizeLabel(value.label);
  if (value.target !== undefined && value.pin !== undefined) fail("duplicate_map_presentation_profile_target");
  const target = normalizeTarget(value.target ?? value.pin);
  const facets = normalizeFacets(value.facets);
  const facetByField = new Map(facets.map((facet) => [facet.field, facet]));
  const styles = normalizeStyles(value.styles);
  const styleIds = new Set(styles.map((style) => style.id));
  const classes = normalizeClasses(value.classes, facetByField, styleIds);
  const classIds = new Set(classes.map((item) => item.id));
  const defaultClassId = identifier(value.defaultClassId, "invalid_map_presentation_profile_default_class");
  if (!classIds.has(defaultClassId)) fail("map_presentation_profile_default_class_not_found");
  const sort = normalizeSort(value.sort, facetByField);

  return {
    id,
    version,
    title,
    semanticTypes,
    label,
    target,
    facets,
    styles,
    classes: [...classes].sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id)),
    defaultClassId,
    sort,
  };
}

export function normalizeMapPresentationProfiles(value) {
  if (!Array.isArray(value) || value.length > 32) fail("invalid_map_presentation_profiles");
  const profiles = value.map(normalizeMapPresentationProfile);
  unique(profiles.map((profile) => profile.id), "duplicate_map_presentation_profile_id");
  return profiles;
}

function normalizeLabel(value) {
  object(value, "invalid_map_presentation_profile_label");
  onlyKeys(value, LABEL_KEYS, "invalid_map_presentation_profile_label_fields");
  const mode = value.mode === undefined ? "attributes" : value.mode;
  if (!["subject_id", "attributes", "none"].includes(mode)) fail("invalid_map_presentation_profile_label_mode");
  return {
    mode,
    fields: fields(value.fields, 1, 16, "invalid_map_presentation_profile_label_fields"),
    fontWeight: integer(value.fontWeight, 400, 700, "invalid_map_presentation_profile_label_weight"),
    sizePx: number(value.sizePx, 8, 32, "invalid_map_presentation_profile_label_size"),
    color: hex(value.color, "invalid_map_presentation_profile_label_color"),
    outlineColor: hex(value.outlineColor, "invalid_map_presentation_profile_label_outline"),
    outlineWidthPx: number(value.outlineWidthPx, 0, 6, "invalid_map_presentation_profile_label_outline_width"),
    backgroundColor: hex(value.backgroundColor ?? "#0c0d12", "invalid_map_presentation_profile_label_background"),
    backgroundOpacity: number(value.backgroundOpacity ?? 0.86, 0, 1, "invalid_map_presentation_profile_label_background_opacity"),
    paddingX: number(value.paddingX ?? 8, 0, 40, "invalid_map_presentation_profile_label_padding"),
    paddingY: number(value.paddingY ?? 5, 0, 40, "invalid_map_presentation_profile_label_padding"),
    maxLength: integer(value.maxLength, 8, 240, "invalid_map_presentation_profile_label_max_length"),
    offsetX: number(value.offsetX, -100, 100, "invalid_map_presentation_profile_label_offset"),
    offsetY: number(value.offsetY, -100, 100, "invalid_map_presentation_profile_label_offset"),
    hideCameraHeightMeters: number(value.hideCameraHeightMeters, 1, 100_000_000, "invalid_map_presentation_profile_label_lod"),
  };
}

function normalizeTarget(value) {
  object(value, "invalid_map_presentation_profile_target");
  onlyKeys(value, TARGET_KEYS, "invalid_map_presentation_profile_target_fields");
  const shared = {
    outlineColor: hex(value.outlineColor, "invalid_map_presentation_profile_target_outline"),
    outlineOpacity: number(value.outlineOpacity, 0, 1, "invalid_map_presentation_profile_target_outline_opacity"),
    outlineWidthPx: number(value.outlineWidthPx, 0, 8, "invalid_map_presentation_profile_target_outline_width"),
    hideCameraHeightMeters: number(value.hideCameraHeightMeters, 1, 100_000_000, "invalid_map_presentation_profile_target_lod"),
  };
  if (value.variant === "surface-fill") {
    if (value.stemHeightMeters !== undefined || value.headSizePx !== undefined || value.stemWidthPx !== undefined) {
      fail("invalid_map_presentation_profile_surface_target_fields");
    }
    return { variant: "surface-fill", ...shared };
  }
  if (value.variant !== "elevated-spike") fail("invalid_map_presentation_profile_target_variant");
  return {
    variant: "elevated-spike",
    stemHeightMeters: number(value.stemHeightMeters, 1, 100_000, "invalid_map_presentation_profile_target_stem_height"),
    headSizePx: number(value.headSizePx, 1, 64, "invalid_map_presentation_profile_target_head_size"),
    stemWidthPx: number(value.stemWidthPx, 0.25, 16, "invalid_map_presentation_profile_target_stem_width"),
    ...shared,
  };
}

function normalizeFacets(value) {
  if (!Array.isArray(value) || value.length < 1 || value.length > 16) fail("invalid_map_presentation_profile_facets");
  const facets = value.map((facet) => {
    object(facet, "invalid_map_presentation_profile_facet");
    onlyKeys(facet, FACET_KEYS, "invalid_map_presentation_profile_facet_fields");
    const values = Array.isArray(facet.values) ? facet.values.map((item) => {
      object(item, "invalid_map_presentation_profile_facet_value");
      onlyKeys(item, FACET_VALUE_KEYS, "invalid_map_presentation_profile_facet_value_fields");
      return {
        value: identifier(item.value, "invalid_map_presentation_profile_facet_value"),
        label: text(item.label, 80, "invalid_map_presentation_profile_facet_value_label"),
        order: integer(item.order, 0, 1000, "invalid_map_presentation_profile_facet_value_order"),
      };
    }) : fail("invalid_map_presentation_profile_facet_values");
    if (values.length < 1 || values.length > 32) fail("invalid_map_presentation_profile_facet_values");
    unique(values.map((item) => item.value), "duplicate_map_presentation_profile_facet_value");
    return {
      id: identifier(facet.id, "invalid_map_presentation_profile_facet_id"),
      field: field(facet.field, "invalid_map_presentation_profile_facet_field"),
      label: text(facet.label, 80, "invalid_map_presentation_profile_facet_label"),
      filterable: boolean(facet.filterable, "invalid_map_presentation_profile_facet_filterable"),
      counter: boolean(facet.counter, "invalid_map_presentation_profile_facet_counter"),
      values: [...values].sort((left, right) => left.order - right.order || left.value.localeCompare(right.value)),
    };
  });
  unique(facets.map((facet) => facet.id), "duplicate_map_presentation_profile_facet_id");
  unique(facets.map((facet) => facet.field), "duplicate_map_presentation_profile_facet_field");
  return facets;
}

function normalizeStyles(value) {
  if (!Array.isArray(value) || value.length < 1 || value.length > 32) fail("invalid_map_presentation_profile_styles");
  const styles = value.map((style) => {
    object(style, "invalid_map_presentation_profile_style");
    onlyKeys(style, STYLE_KEYS, "invalid_map_presentation_profile_style_fields");
    return {
      id: identifier(style.id, "invalid_map_presentation_profile_style_id"),
      color: hex(style.color, "invalid_map_presentation_profile_style_color"),
      opacity: number(style.opacity, 0, 1, "invalid_map_presentation_profile_style_opacity"),
    };
  });
  unique(styles.map((style) => style.id), "duplicate_map_presentation_profile_style_id");
  return styles;
}

function normalizeClasses(value, facetByField, styleIds) {
  if (!Array.isArray(value) || value.length < 1 || value.length > 64) fail("invalid_map_presentation_profile_classes");
  const classes = value.map((item) => {
    object(item, "invalid_map_presentation_profile_class");
    onlyKeys(item, CLASS_KEYS, "invalid_map_presentation_profile_class_fields");
    if (!Array.isArray(item.match) || item.match.length > 8) fail("invalid_map_presentation_profile_class_match");
    const match = item.match.map((condition) => {
      object(condition, "invalid_map_presentation_profile_class_condition");
      onlyKeys(condition, MATCH_KEYS, "invalid_map_presentation_profile_class_condition_fields");
      const matchField = field(condition.field, "invalid_map_presentation_profile_class_condition_field");
      const equals = identifier(condition.equals, "invalid_map_presentation_profile_class_condition_value");
      const facet = facetByField.get(matchField);
      if (!facet || !facet.values.some((item) => item.value === equals)) {
        fail("map_presentation_profile_class_condition_not_declared");
      }
      return { field: matchField, equals };
    });
    const styleId = identifier(item.styleId, "invalid_map_presentation_profile_class_style");
    if (!styleIds.has(styleId)) fail("map_presentation_profile_class_style_not_found");
    return {
      id: identifier(item.id, "invalid_map_presentation_profile_class_id"),
      label: text(item.label, 80, "invalid_map_presentation_profile_class_label"),
      priority: integer(item.priority, -10000, 10000, "invalid_map_presentation_profile_class_priority"),
      match,
      styleId,
      renderable: boolean(item.renderable, "invalid_map_presentation_profile_class_renderable"),
    };
  });
  unique(classes.map((item) => item.id), "duplicate_map_presentation_profile_class_id");
  return classes;
}

function normalizeSort(value, facetByField) {
  if (!Array.isArray(value) || value.length > 16) fail("invalid_map_presentation_profile_sort");
  const sort = value.map((item) => {
    object(item, "invalid_map_presentation_profile_sort_item");
    onlyKeys(item, SORT_KEYS, "invalid_map_presentation_profile_sort_fields");
    const sortField = field(item.field, "invalid_map_presentation_profile_sort_field");
    const facet = facetByField.get(sortField);
    if (!facet || !Array.isArray(item.order) || item.order.length !== facet.values.length) {
      fail("map_presentation_profile_sort_not_declared");
    }
    const order = item.order.map((entry) => identifier(entry, "invalid_map_presentation_profile_sort_order"));
    unique(order, "duplicate_map_presentation_profile_sort_order");
    if (order.some((entry) => !facet.values.some((item) => item.value === entry))) {
      fail("map_presentation_profile_sort_not_declared");
    }
    return { field: sortField, order };
  });
  unique(sort.map((item) => item.field), "duplicate_map_presentation_profile_sort_field");
  return sort;
}

function onlyKeys(value, allowed, code) {
  if (Object.keys(value).some((key) => !allowed.has(key))) fail(code);
}

function object(value, code) {
  if (!value || typeof value !== "object" || Array.isArray(value)) fail(code);
}

function identifier(value, code) {
  const normalized = text(value, 160, code);
  if (!IDENTIFIER.test(normalized)) fail(code);
  return normalized;
}

function field(value, code) {
  const normalized = text(value, 128, code);
  if (!FIELD.test(normalized)) fail(code);
  return normalized;
}

function identifiers(value, min, max, code) {
  if (!Array.isArray(value) || value.length < min || value.length > max) fail(code);
  const normalized = value.map((item) => identifier(item, code));
  unique(normalized, code);
  return normalized;
}

function fields(value, min, max, code) {
  if (!Array.isArray(value) || value.length < min || value.length > max) fail(code);
  const normalized = value.map((item) => field(item, code));
  unique(normalized, code);
  return normalized;
}

function text(value, max, code) {
  if (typeof value !== "string") fail(code);
  const normalized = value.trim();
  if (!normalized || normalized.length > max) fail(code);
  return normalized;
}

function hex(value, code) {
  const normalized = text(value, 7, code).toLowerCase();
  if (!/^#[0-9a-f]{6}$/.test(normalized)) fail(code);
  return normalized;
}

function number(value, min, max, code) {
  if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) fail(code);
  return value;
}

function integer(value, min, max, code) {
  const normalized = number(value, min, max, code);
  if (!Number.isInteger(normalized)) fail(code);
  return normalized;
}

function boolean(value, code) {
  if (typeof value !== "boolean") fail(code);
  return value;
}

function unique(values, code) {
  if (new Set(values).size !== values.length) fail(code);
}

function fail(code) {
  throw Object.assign(new Error(code), { statusCode: 400 });
}
