import type { Feature, FeatureCollection, Geometry, Position } from "geojson";
import type { GeoJSONSource, Map as MapLibreMap, PaddingOptions } from "maplibre-gl";
import { SOURCE_LAYERS, type WaterNetworkSourceId } from "./sources";
import {
  LAYER_GROUP_GEOMETRIES,
  applyLayerGroupStyle as applyStyle,
  resetLayerGroupStyle as resetStyle,
  validateLayerGroupStylePatch,
  type LayerGroupId,
  type LayerGroupStylePatch
} from "./layer-group-style";
import {
  clearPipeFlowCalculations as clearFlowCalculations,
  createFlowOverlayState,
  hideFlowOverlay,
  replacePipeFlowCalculations as replaceFlowCalculations,
  showFlowOverlay,
  updatePipeFlowCalculation as updateFlowCalculation,
  type FlowOverlayState,
  type PipeFlowCalculation,
  type PipeFlowSummary
} from "./flow-overlay";
import {
  MAX_PIPE_FEATURES,
  MAX_MAP_FEATURE_IDS,
  createMapFeatureWfsUrl,
  normalizeMapFeatureCollection,
  parseMapFeatureQuery
} from "./map-feature-query";
import {
  applyValueLabelStyle,
  clearValueLabels,
  type ValueLabelSourceId,
  type ValueLabelStyle
} from "./value-label";
import {
  SCADA_ANALYSIS_SOURCE_ID,
  createEmptyScadaAnalysisCollection,
  createScadaAnalysisCollection,
  ensureScadaAnalysisLayers,
  type ScadaAnalysisItem,
  type ScadaAnalysisRenderResult
} from "./scada-analysis";

export type FeatureTarget = { sourceId: WaterNetworkSourceId; featureId: string };
export type WorkbenchMapErrorCode =
  | "MAP_NOT_READY"
  | "FEATURE_NOT_FOUND"
  | "WFS_UNAVAILABLE"
  | "SCADA_FEATURES_NOT_FOUND"
  | "ACTION_SUPERSEDED"
  | "FLOW_UNAVAILABLE"
  | "INVALID_FLOW_CALCULATION"
  | "INVALID_STYLE_PATCH";

export type WorkbenchMapControllerState = {
  target: FeatureTarget | null;
  valueLabels: Partial<Record<ValueLabelSourceId, ValueLabelStyle>>;
  pending: boolean;
  errorCode: WorkbenchMapErrorCode | null;
  flowVisible: boolean;
  flowSummary: PipeFlowSummary;
  styledGroups: LayerGroupId[];
  scadaAnalysis: ScadaAnalysisRenderResult | null;
};

export type ValueLabelRequest = Omit<ValueLabelStyle, "unit"> & { unit?: string };
export type WorkbenchMapCommands = {
  zoomToFeature: (target: FeatureTarget) => Promise<void>;
  locateAndHighlight: (target: FeatureTarget) => Promise<void>;
  highlight: (target: FeatureTarget) => Promise<void>;
  clearHighlight: () => void;
  setFlowVisible: (visible: boolean) => Promise<void>;
  replacePipeFlowCalculations: (records: readonly PipeFlowCalculation[]) => Promise<PipeFlowSummary | null>;
  updatePipeFlowCalculation: (record: PipeFlowCalculation) => PipeFlowSummary | null;
  clearPipeFlowCalculations: () => Promise<void>;
  showValueLabel: (request: ValueLabelRequest) => Promise<void>;
  clearValueLabel: (sourceId?: ValueLabelSourceId) => void;
  applyLayerGroupStyle: (groupId: LayerGroupId, patch: LayerGroupStylePatch) => void;
  resetLayerGroupStyle: (groupId: LayerGroupId) => void;
  renderScadaAnalysis: (items: ScadaAnalysisItem[], signal?: AbortSignal) => Promise<ScadaAnalysisRenderResult>;
  clearScadaAnalysis: () => { cleared: true };
  resetAll: () => void;
};

export type WorkbenchMapControllerOptions = {
  getMap: () => MapLibreMap | null;
  isReady: () => boolean;
  getPadding: () => PaddingOptions;
  fetchFeatures?: (sourceId: WaterNetworkSourceId, featureIds?: string[], signal?: AbortSignal) => Promise<FeatureCollection>;
  reducedMotion?: () => boolean;
};

export class WorkbenchMapController implements WorkbenchMapCommands {
  private state: WorkbenchMapControllerState = {
    target: null,
    valueLabels: {},
    pending: false,
    errorCode: null,
    flowVisible: false,
    flowSummary: createEmptyPipeFlowSummary(),
    styledGroups: [],
    scadaAnalysis: null
  };
  private listeners = new Set<() => void>();
  private highlightedTarget: FeatureTarget | null = null;
  private cachedFeatures = new Map<string, Feature>();
  private flowState: FlowOverlayState = createFlowOverlayState();
  private flowUpdateRevision = 0;
  private scadaAnalysisRevision = 0;

  constructor(private readonly options: WorkbenchMapControllerOptions) {}

  subscribe = (listener: () => void) => {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  };

  getSnapshot = () => this.state;

  async zoomToFeature(target: FeatureTarget) {
    const feature = await this.resolveTarget(target);
    const map = this.requireMap();
    if (!feature || !map) return;
    this.moveCameraToFeature(map, feature);
  }

  async locateAndHighlight(target: FeatureTarget) {
    const feature = await this.resolveTarget(target);
    const map = this.requireMap();
    if (!feature || !map) return;
    if (!this.moveCameraToFeature(map, feature)) return;
    this.setHighlight(target);
  }

  private moveCameraToFeature(map: MapLibreMap, feature: Feature) {
    const coordinates = collectCoordinates(feature.geometry);
    if (!coordinates.length) {
      this.setError("FEATURE_NOT_FOUND");
      return false;
    }
    const duration = this.options.reducedMotion?.() ? 0 : 600;
    if (feature.geometry.type === "Point") {
      map.easeTo({
        center: coordinates[0] as [number, number],
        zoom: 18,
        padding: this.options.getPadding(),
        duration
      });
    } else {
      const bounds = coordinates.reduce(
        (acc, coordinate) => [Math.min(acc[0], coordinate[0]), Math.min(acc[1], coordinate[1]), Math.max(acc[2], coordinate[0]), Math.max(acc[3], coordinate[1])],
        [Infinity, Infinity, -Infinity, -Infinity]
      );
      map.fitBounds([[bounds[0], bounds[1]], [bounds[2], bounds[3]]], {
        padding: this.options.getPadding(),
        maxZoom: 18,
        duration
      });
    }
    this.patchState({ errorCode: null });
    return true;
  }

  async highlight(target: FeatureTarget) {
    const feature = await this.resolveTarget(target);
    if (feature) this.setHighlight(target);
  }

  clearHighlight = () => {
    const map = this.options.getMap();
    if (map && this.highlightedTarget) {
      map.removeFeatureState(toFeatureStateTarget(this.highlightedTarget), "highlighted");
    }
    this.highlightedTarget = null;
    this.patchState({ target: null, errorCode: null });
  };

  async setFlowVisible(visible: boolean) {
    const map = this.requireMap();
    if (!map) return;
    if (!visible) {
      hideFlowOverlay(map, this.flowState);
      this.patchState({ flowVisible: false, errorCode: null });
      return;
    }
    this.patchState({ pending: true, errorCode: null });
    try {
      await showFlowOverlay(
        map,
        this.flowState,
        this.options.reducedMotion?.()
      );
      this.patchState({ pending: false, flowVisible: true });
    } catch {
      hideFlowOverlay(map, this.flowState);
      this.patchState({ pending: false, flowVisible: false, errorCode: "FLOW_UNAVAILABLE" });
    }
  }

  async replacePipeFlowCalculations(
    records: readonly PipeFlowCalculation[]
  ): Promise<PipeFlowSummary | null> {
    const map = this.requireMap();
    if (!map) return null;
    const revision = ++this.flowUpdateRevision;
    this.patchState({ pending: true, errorCode: null });
    try {
      const summary = await replaceFlowCalculations(map, this.flowState, records);
      if (revision === this.flowUpdateRevision) {
        this.patchState({ pending: false, flowSummary: summary, errorCode: null });
      }
      return summary;
    } catch {
      if (revision === this.flowUpdateRevision) {
        this.patchState({ pending: false, errorCode: "FLOW_UNAVAILABLE" });
      }
      return null;
    }
  }

  updatePipeFlowCalculation(
    record: PipeFlowCalculation
  ): PipeFlowSummary | null {
    const map = this.requireMap();
    if (!map) return null;
    this.flowUpdateRevision += 1;
    try {
      const summary = updateFlowCalculation(map, this.flowState, record);
      this.patchState({ pending: false, flowSummary: summary, errorCode: null });
      return summary;
    } catch {
      this.patchState({
        pending: false,
        errorCode: "INVALID_FLOW_CALCULATION"
      });
      return null;
    }
  }

  async clearPipeFlowCalculations() {
    const map = this.requireMap();
    if (!map) return;
    const revision = ++this.flowUpdateRevision;
    this.patchState({ pending: true, errorCode: null });
    try {
      await clearFlowCalculations(map, this.flowState);
      if (revision === this.flowUpdateRevision) {
        this.patchState({
          pending: false,
          flowSummary: createEmptyPipeFlowSummary(),
          errorCode: null
        });
      }
    } catch {
      if (revision === this.flowUpdateRevision) {
        this.patchState({ pending: false, errorCode: "FLOW_UNAVAILABLE" });
      }
    }
  }

  async showValueLabel(request: ValueLabelRequest) {
    const map = this.requireMap();
    if (!map) return;
    const style: ValueLabelStyle = { ...request, unit: request.unit ?? "" };
    try {
      applyValueLabelStyle(map, style);
      this.patchState({
        valueLabels: {
          ...this.state.valueLabels,
          [style.sourceId]: style
        },
        errorCode: null
      });
    } catch {
      this.setError("INVALID_STYLE_PATCH");
    }
  }

  clearValueLabel = (sourceId?: ValueLabelSourceId) => {
    const map = this.options.getMap();
    if (map) clearValueLabels(map, sourceId);
    if (!sourceId) {
      this.patchState({ valueLabels: {} });
      return;
    }
    const valueLabels = { ...this.state.valueLabels };
    delete valueLabels[sourceId];
    this.patchState({ valueLabels });
  };

  applyLayerGroupStyle(groupId: LayerGroupId, patch: LayerGroupStylePatch) {
    const map = this.requireMap();
    if (!map) return;
    if (!validateLayerGroupStylePatch(groupId, patch)) return this.setError("INVALID_STYLE_PATCH");
    applyStyle(map, groupId, patch);
    this.patchState({ styledGroups: [...new Set([...this.state.styledGroups, groupId])], errorCode: null });
  }

  resetLayerGroupStyle(groupId: LayerGroupId) {
    const map = this.requireMap();
    if (!map || !(groupId in LAYER_GROUP_GEOMETRIES)) return;
    resetStyle(map, groupId);
    this.patchState({ styledGroups: this.state.styledGroups.filter((item) => item !== groupId), errorCode: null });
  }

  async renderScadaAnalysis(
    items: ScadaAnalysisItem[],
    signal?: AbortSignal
  ): Promise<ScadaAnalysisRenderResult> {
    const revision = ++this.scadaAnalysisRevision;
    const map = this.options.getMap();
    if (!map || !this.options.isReady()) {
      this.setError("MAP_NOT_READY");
      throw new Error("MAP_NOT_READY");
    }
    this.patchState({ pending: true, errorCode: null });

    let sourceCollection: FeatureCollection;
    try {
      sourceCollection = await this.fetchFeatures(
        "scada",
        items.map((item) => item.sensor_id),
        signal
      );
    } catch {
      if (revision === this.scadaAnalysisRevision) {
        this.patchState({ pending: false, errorCode: "WFS_UNAVAILABLE" });
      }
      throw new Error(
        revision === this.scadaAnalysisRevision
          ? "WFS_UNAVAILABLE"
          : "ACTION_SUPERSEDED"
      );
    }
    if (revision !== this.scadaAnalysisRevision) {
      throw new Error("ACTION_SUPERSEDED");
    }

    const next = createScadaAnalysisCollection(sourceCollection, items);
    if (next.collection.features.length === 0) {
      this.patchState({
        pending: false,
        errorCode: "SCADA_FEATURES_NOT_FOUND"
      });
      throw new Error("SCADA_FEATURES_NOT_FOUND");
    }
    const source = map.getSource(
      SCADA_ANALYSIS_SOURCE_ID
    ) as GeoJSONSource | undefined;
    if (!source) {
      this.patchState({ pending: false, errorCode: "MAP_NOT_READY" });
      throw new Error("MAP_NOT_READY");
    }

    try {
      ensureScadaAnalysisLayers(map);
      source.setData(next.collection);
    } catch {
      this.patchState({ pending: false, errorCode: "MAP_NOT_READY" });
      throw new Error("MAP_NOT_READY");
    }

    const result: ScadaAnalysisRenderResult = {
      ...next.result,
      fitted: true
    };
    this.patchState({
      pending: false,
      errorCode: null,
      scadaAnalysis: result
    });
    const coordinates = next.collection.features.map(
      (feature) => feature.geometry.coordinates
    );
    if (coordinates.length === 1) {
      map.easeTo({
        center: [coordinates[0][0], coordinates[0][1]],
        zoom: 18,
        padding: this.options.getPadding(),
        duration: this.options.reducedMotion?.() ? 0 : 600
      });
    } else {
      const bounds = coordinates.reduce<[number, number, number, number]>(
        (value, coordinate) => [
          Math.min(value[0], coordinate[0]),
          Math.min(value[1], coordinate[1]),
          Math.max(value[2], coordinate[0]),
          Math.max(value[3], coordinate[1])
        ],
        [Infinity, Infinity, -Infinity, -Infinity]
      );
      map.fitBounds(
        [
          [bounds[0], bounds[1]],
          [bounds[2], bounds[3]]
        ],
        {
          padding: this.options.getPadding(),
          maxZoom: 18,
          duration: this.options.reducedMotion?.() ? 0 : 600
        }
      );
    }
    return result;
  }

  clearScadaAnalysis() {
    this.scadaAnalysisRevision += 1;
    const map = this.options.getMap();
    (map?.getSource(SCADA_ANALYSIS_SOURCE_ID) as GeoJSONSource | undefined)?.setData(
      createEmptyScadaAnalysisCollection()
    );
    this.patchState({ scadaAnalysis: null, pending: false, errorCode: null });
    return { cleared: true as const };
  }

  resetAll = () => {
    this.clearHighlight();
    this.clearValueLabel();
    this.clearScadaAnalysis();
    const map = this.options.getMap();
    if (map) {
      hideFlowOverlay(map, this.flowState);
      this.flowUpdateRevision += 1;
      void clearFlowCalculations(map, this.flowState);
      (Object.keys(LAYER_GROUP_GEOMETRIES) as LayerGroupId[]).forEach((groupId) => resetStyle(map, groupId));
    }
    this.patchState({
      target: null,
      valueLabels: {},
      pending: false,
      errorCode: null,
      flowVisible: false,
      flowSummary: createEmptyPipeFlowSummary(),
      styledGroups: [],
      scadaAnalysis: null
    });
  };

  destroy() {
    this.flowUpdateRevision += 1;
    const map = this.options.getMap();
    if (map) hideFlowOverlay(map, this.flowState);
    this.listeners.clear();
  }

  private async resolveTarget(target: FeatureTarget) {
    const map = this.requireMap();
    if (!map) return null;
    const key = `${target.sourceId}:${target.featureId}`;
    const cached = this.cachedFeatures.get(key);
    if (cached) return cached;
    this.patchState({ pending: true, errorCode: null });
    try {
      const collection = await this.fetchFeatures(target.sourceId, [target.featureId]);
      const feature = collection.features[0];
      if (!feature) {
        this.patchState({ pending: false, errorCode: "FEATURE_NOT_FOUND" });
        return null;
      }
      this.cachedFeatures.set(key, feature);
      this.patchState({ pending: false });
      return feature;
    } catch {
      this.patchState({ pending: false, errorCode: "WFS_UNAVAILABLE" });
      return null;
    }
  }

  private async fetchFeatures(sourceId: WaterNetworkSourceId, featureIds?: string[], signal?: AbortSignal) {
    if (this.options.fetchFeatures) return this.options.fetchFeatures(sourceId, featureIds, signal);
    const parsed = parseMapFeatureQuery({ sourceId, featureIds });
    if (!parsed.ok) throw new Error(parsed.code);
    const response = await fetch(createMapFeatureWfsUrl(parsed.value), {
      headers: { Accept: "application/geo+json, application/json" },
      signal
    });
    if (!response.ok) throw new Error("WFS_UNAVAILABLE");
    const maxFeatures = featureIds ? MAX_MAP_FEATURE_IDS : MAX_PIPE_FEATURES;
    const collection = normalizeMapFeatureCollection(await response.json(), maxFeatures);
    if (!collection) throw new Error("WFS_UNAVAILABLE");
    return collection;
  }

  private setHighlight(target: FeatureTarget) {
    const map = this.requireMap();
    if (!map) return;
    if (this.highlightedTarget) map.removeFeatureState(toFeatureStateTarget(this.highlightedTarget), "highlighted");
    map.setFeatureState(toFeatureStateTarget(target), { highlighted: true });
    this.highlightedTarget = target;
    this.patchState({ target, errorCode: null });
  }

  private requireMap() {
    const map = this.options.getMap();
    if (!map || !this.options.isReady()) {
      this.setError("MAP_NOT_READY");
      return null;
    }
    return map;
  }

  private setError(errorCode: WorkbenchMapErrorCode) {
    this.patchState({ errorCode });
  }

  private patchState(patch: Partial<WorkbenchMapControllerState>) {
    this.state = { ...this.state, ...patch };
    this.listeners.forEach((listener) => listener());
  }
}

function createEmptyPipeFlowSummary(): PipeFlowSummary {
  return { total: 0, forward: 0, reverse: 0, stopped: 0, invalid: 0 };
}

function toFeatureStateTarget(target: FeatureTarget) {
  return { source: target.sourceId, sourceLayer: SOURCE_LAYERS[target.sourceId], id: target.featureId };
}

function collectCoordinates(geometry: Geometry | null): Position[] {
  if (!geometry || geometry.type === "GeometryCollection") return [];
  const coordinates: Position[] = [];
  const visit = (value: unknown) => {
    if (Array.isArray(value) && value.length >= 2 && typeof value[0] === "number" && typeof value[1] === "number") coordinates.push(value as Position);
    else if (Array.isArray(value)) value.forEach(visit);
  };
  visit(geometry.coordinates);
  return coordinates;
}
