import type { MapRuntimeFact } from "./useMapDataProductRuntime.js";

export type MapPresentationFacetValue = {
  value: string;
  label: string;
  order: number;
};

export type MapPresentationFacet = {
  id: string;
  field: string;
  label: string;
  filterable: boolean;
  counter: boolean;
  values: MapPresentationFacetValue[];
};

export type MapPresentationStyle = {
  id: string;
  color: string;
  opacity: number;
};

export type MapPresentationClass = {
  id: string;
  label: string;
  priority: number;
  match: Array<{ field: string; equals: string }>;
  styleId: string;
  renderable: boolean;
};

export type MapPresentationProfile = {
  id: string;
  version: string;
  title: string;
  semanticTypes: string[];
  label: {
    mode: "subject_id" | "attributes" | "none";
    fields: string[];
    fontWeight: number;
    sizePx: number;
    color: string;
    outlineColor: string;
    outlineWidthPx: number;
    backgroundColor: string;
    backgroundOpacity: number;
    paddingX: number;
    paddingY: number;
    maxLength: number;
    offsetX: number;
    offsetY: number;
    hideCameraHeightMeters: number;
  };
  target: ({
    variant: "elevated-spike";
    stemHeightMeters: number;
    headSizePx: number;
    stemWidthPx: number;
  } | {
    variant: "surface-fill";
  }) & {
    outlineColor: string;
    outlineOpacity: number;
    outlineWidthPx: number;
    hideCameraHeightMeters: number;
  };
  facets: MapPresentationFacet[];
  styles: MapPresentationStyle[];
  classes: MapPresentationClass[];
  defaultClassId: string;
  sort: Array<{ field: string; order: string[] }>;
};

export type MapSubjectFilterState = {
  /** False is an explicit empty map state. It must never be normalized to all. */
  visible: boolean;
  /** Missing facet = no constraint; an explicitly empty facet = match nothing. */
  facets: Record<string, string[]>;
};

/** Application view state is keyed by stable binding id, never by editable labels. */
export type MapPresentationFilters = Record<string, MapSubjectFilterState>;

/**
 * Application manifests persisted before profile v1.1 used the internal key
 * `pin`. Normalize that storage shape before the first React render so an old
 * application cannot crash while it is being upgraded to the public `target`
 * contract through MCP.
 */
export function normalizeClientMapPresentationProfiles(profiles: MapPresentationProfile[]) {
  return profiles.flatMap((profile) => {
    const legacyPin = (profile as MapPresentationProfile & { pin?: MapPresentationProfile["target"] }).pin;
    const target = profile.target ?? legacyPin;
    if (!target) return [];
    const normalized = { ...profile, target } as MapPresentationProfile & { pin?: MapPresentationProfile["target"] };
    delete normalized.pin;
    return [normalized];
  });
}

export function mapPresentationBindingIsAll(bindingId: string, filters: MapPresentationFilters) {
  const state = filters[bindingId];
  return state?.visible !== false && Object.keys(state?.facets ?? {}).length === 0;
}

/**
 * Apply one interactive facet-chip transition without collapsing the storage
 * contract. A missing field means unconstrained, while an explicitly persisted
 * empty array remains available to represent an intentional match-nothing view.
 */
export function toggleMapPresentationFacetSelection(
  facets: Record<string, string[]>,
  field: string,
  value: string,
) {
  const selected = facets[field];
  if (!selected?.includes(value)) {
    return { ...facets, [field]: [...(selected ?? []), value] };
  }

  const nextSelected = selected.filter((item) => item !== value);
  if (nextSelected.length > 0) return { ...facets, [field]: nextSelected };

  const { [field]: _removed, ...unconstrained } = facets;
  return unconstrained;
}

export function mapPresentationProfileForFact(
  profiles: MapPresentationProfile[],
  presentationProfileId: string | undefined,
  semanticType: string,
) {
  const exact = presentationProfileId
    ? profiles.find((profile) => profile.id === presentationProfileId)
    : undefined;
  if (exact?.semanticTypes.includes(semanticType)) return exact;
  return profiles.find((profile) => profile.semanticTypes.includes(semanticType));
}

export function resolveMapPresentationClass(fact: MapRuntimeFact, profile: MapPresentationProfile) {
  const classes = [...profile.classes].sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id));
  return classes.find((item) => item.match.every((condition) => (
    normalizedFacetValue(fact.attributes[condition.field]) === condition.equals
  ))) ?? classes.find((item) => item.id === profile.defaultClassId) ?? classes.at(-1);
}

export function resolveMapPresentationStyle(profile: MapPresentationProfile, presentationClass?: MapPresentationClass) {
  const selected = presentationClass ?? profile.classes.find((item) => item.id === profile.defaultClassId);
  return profile.styles.find((style) => style.id === selected?.styleId) ?? profile.styles[0];
}

export function mapRuntimeDisplayLabel(fact: MapRuntimeFact, profile?: MapPresentationProfile) {
  if (profile?.label.mode === "subject_id") return fact.sourceId;
  const fields = profile?.label.fields ?? ["display_name", "label", "name", "title"];
  for (const key of fields) {
    const value = fact.attributes[key];
    if (typeof value === "string" && value.trim()) {
      const normalized = value.trim();
      const limit = profile?.label.maxLength ?? 80;
      return normalized.length > limit ? `${normalized.slice(0, Math.max(1, limit - 1))}…` : normalized;
    }
  }
  return fact.sourceId;
}

export function mapRuntimeFactIsRenderable(fact: MapRuntimeFact, profile: MapPresentationProfile) {
  return Boolean(fact.geometry) && resolveMapPresentationClass(fact, profile)?.renderable === true;
}

export function mapRuntimeFactIsVisible(
  fact: MapRuntimeFact,
  profile: MapPresentationProfile,
  filters: MapPresentationFilters,
  bindingId: string,
) {
  return mapRuntimeFactIsRenderable(fact, profile) && mapFactMatchesFilters(fact, profile, filters, bindingId);
}

export function mapFactMatchesFilters(
  fact: MapRuntimeFact,
  profile: MapPresentationProfile,
  filters: MapPresentationFilters,
  bindingId: string,
) {
  const state = filters[bindingId];
  if (state?.visible === false) return false;

  const selectedFacets = profile.facets.flatMap((facet) => {
    const selected = state?.facets?.[facet.field];
    if (selected === undefined) return [];
    return [{ facet, selected }];
  });

  // Persisted empty arrays are an explicit match-nothing state. Interactive
  // deselection removes the field instead, so this branch is only reached for
  // a deliberately saved empty view.
  if (selectedFacets.some(({ selected }) => selected.length === 0)) return false;
  if (selectedFacets.length === 0) return true;

  // Chips form one global union. This mirrors the objects window: choosing
  // values from two categories expands the visible set instead of requiring a
  // fact to satisfy both categories simultaneously.
  return selectedFacets.some(({ facet, selected }) => (
    mapFactParticipatesInFacet(fact, profile, facet)
    && selected.includes(normalizedFacetValue(fact.attributes[facet.field]))
  ));
}

export function compareMapRuntimeFacts(left: MapRuntimeFact, right: MapRuntimeFact, profile: MapPresentationProfile) {
  for (const rule of profile.sort) {
    const leftRank = sortRank(rule.order, normalizedFacetValue(left.attributes[rule.field]));
    const rightRank = sortRank(rule.order, normalizedFacetValue(right.attributes[rule.field]));
    if (leftRank !== rightRank) return leftRank - rightRank;
  }
  return mapRuntimeDisplayLabel(left, profile).localeCompare(mapRuntimeDisplayLabel(right, profile), "ru");
}

export function mapPresentationFacetCounts(
  facts: MapRuntimeFact[],
  profile: MapPresentationProfile,
) {
  return Object.fromEntries(profile.facets.map((facet) => {
    const counts = Object.fromEntries(facet.values.map((item) => [item.value, 0]));
    for (const fact of facts) {
      if (!mapFactParticipatesInFacet(fact, profile, facet)) continue;
      const value = normalizedFacetValue(fact.attributes[facet.field]);
      if (Object.hasOwn(counts, value)) counts[value] += 1;
    }
    return [facet.field, counts];
  })) as Record<string, Record<string, number>>;
}

/**
 * `signal_state` and `movement_state` remain orthogonal Data Product facts.
 * The operational Map, however, must not present a stale last speed as a
 * current movement state. When both canonical facets exist, the movement
 * facet is therefore scoped to currently active subjects. Other profiles and
 * fields keep their ordinary independent-facet behaviour.
 */
function mapFactParticipatesInFacet(
  fact: MapRuntimeFact,
  profile: MapPresentationProfile,
  facet: MapPresentationFacet,
) {
  if (facet.field !== "movement_state") return true;
  if (!profile.facets.some((item) => item.field === "signal_state")) return true;
  return normalizedFacetValue(fact.attributes.signal_state) === "active";
}

function normalizedFacetValue(value: unknown) {
  return typeof value === "string" ? value.trim().toLowerCase() : "unknown";
}

function sortRank(order: string[], value: string) {
  const index = order.indexOf(value);
  return index === -1 ? order.length : index;
}
