"use client";

/**
 * MassingScene — client-only Three.js building-massing sandbox for the ПТИЦА
 * cockpit. React port of the prototype `ptica-redesign/massing.js`.
 *
 * Builds a generative building mass from the parcel area + a regulation КСИТ
 * (FAR) target and user-controlled study inputs (этажность / секций / время
 * суток). A shadow-casting sun driven by the time-of-day slider doubles as a
 * basic insolation preview. All metrics (GFA, КСИТ-факт) are computed from the
 * inputs — это интерактивная песочница, not parcel-specific regulation data.
 *
 * Loaded exclusively through MassingViewer via `dynamic(ssr:false)` so Three.js
 * never touches the server. The renderer/scene/RAF live in a single effect with
 * full teardown (cancel RAF, dispose geometries/materials/renderer, remove the
 * canvas, disconnect ResizeObserver) on unmount, so closing the drawer frees GPU
 * resources.
 */

import { useCallback, useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";

import styles from "@/app/site-finder/analysis/[cad]/ptica/ptica.module.css";

import { sunFromHour } from "./sun";
import { disposeObject } from "./three-utils";

export interface MassingSceneProps {
  /** Real parcel area in m² when available (fallback ≈ 6800). */
  areaM2?: number;
  /** КСИТ-цель (FAR target) — real НСПД-регламент when present, else default. */
  maxFar?: number;
  /** True when `maxFar` is the real НСПД ПЗЗ value (drives the caption). */
  farIsReal?: boolean;
  /**
   * Compact preview mode — renders ONLY the autorotating viewport (no sliders /
   * metrics), with all user interaction disabled (hands-off mini massing for the
   * Инсоляционная матрица lower-grid card).
   */
  preview?: boolean;
  /**
   * Stage 2b (#1965) — fired with the recomputed model whenever the BUILDING
   * program changes (floors / sections), including the initial mount build. NOT
   * fired on the time-of-day (sun) slider — that only moves light, not mass. A
   * no-op when absent, so the /ptica cockpit + preview/drawer keep their existing
   * behaviour. Section7 wires this to a debounced live financial recompute.
   */
  onModelChange?: (model: MassingModel) => void;
}

// ── constants (scene units = metres) ──────────────────────────────────────────
const FLOOR_H = 3; // metres per floor
const SETBACK = 7; // metres kept clear from parcel edges
const PODIUM_H = 4; // stylobate height (metres)
const DEFAULT_AREA = 6800; // fallback parcel area (m²)
const FALLBACK_FAR = 3.5;

const FLOORS_MIN = 3;
const FLOORS_MAX = 40;
const SECTIONS_MIN = 1;
const SECTIONS_MAX = 6;
const HOUR_MIN = 6;
const HOUR_MAX = 20;

// ── derived massing model ─────────────────────────────────────────────────────

export interface MassingModel {
  floors: number;
  sections: number;
  height: number;
  totalFootprint: number;
  towerFootprints: number[];
  gfa: number;
  farActual: number;
  maxFar: number;
  parcelArea: number;
  parcelW: number;
  parcelD: number;
}

/**
 * Pure model: parcel area + FAR target + slider inputs → geometry params + metrics.
 * Footprint per floor is sized to reach the КСИТ target GFA, capped by the
 * buildable footprint (usable area inside the setbacks).
 */
function computeModel(
  parcelArea: number,
  maxFar: number,
  floors: number,
  sections: number,
): MassingModel {
  const f = Math.max(1, Math.round(floors));
  const n = Math.max(1, Math.round(sections));

  // approximate a square parcel from its area (scene is a study model, not a
  // surveyed polygon) and derive the usable buildable area inside the setbacks.
  const side = Math.sqrt(parcelArea);
  const parcelW = side;
  const parcelD = side;
  const usableW = Math.max(8, parcelW - 2 * SETBACK);
  const usableD = Math.max(8, parcelD - 2 * SETBACK);
  const maxFootprint = usableW * usableD;

  const targetGFA = parcelArea * maxFar; // КСИТ ёмкость
  const neededPerFloor = targetGFA / f;
  const totalFootprint = Math.min(neededPerFloor, maxFootprint);

  // split footprint across N towers with gentle deterministic variation
  // (weights sum to 1 → Σ footprints == totalFootprint).
  const weights: number[] = [];
  let wsum = 0;
  for (let i = 0; i < n; i++) {
    const w = 1 + 0.18 * Math.sin(i * 2.4 + 0.7);
    weights.push(w);
    wsum += w;
  }
  const towerFootprints = weights.map((w) => (totalFootprint * w) / wsum);

  const height = f * FLOOR_H;
  const gfa = totalFootprint * f;
  const farActual = gfa / parcelArea;

  return {
    floors: f,
    sections: n,
    height,
    totalFootprint,
    towerFootprints,
    gfa,
    farActual,
    maxFar,
    parcelArea,
    parcelW,
    parcelD,
  };
}

// ── sun slider → direction + colour ───────────────────────────────────────────
// `sunFromHour` + `SunState` now live in ./sun (shared with the §7 «Концепция»
// 3D viewer). `placeSun` below imports it.

// ── formatting ────────────────────────────────────────────────────────────────

function fmtRu(n: number): string {
  return Math.round(n).toLocaleString("ru-RU");
}

function fmtHour(hour: number): string {
  const h = Math.floor(hour);
  const m = Math.round((hour - h) * 60);
  return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}

// ── building meshes ───────────────────────────────────────────────────────────

const COLOR_ACCENT = 0x7fd0ee; // --accent-cyan
const COLOR_SLATE = 0x33424f;
const BG_COLOR = 0x060f16; // --bg

/** Returns a THREE.Group with podium + towers + edge accents for the model. */
function buildMass(model: MassingModel): THREE.Group {
  const group = new THREE.Group();
  group.name = "mass";

  const accent = new THREE.Color(COLOR_ACCENT);
  const slate = new THREE.Color(COLOR_SLATE);

  const usableW = Math.max(8, model.parcelW - 2 * SETBACK);
  const usableD = Math.max(8, model.parcelD - 2 * SETBACK);

  // thin podium / stylobate covering the usable area
  const podiumGeo = new THREE.BoxGeometry(usableW, PODIUM_H, usableD);
  const podiumMat = new THREE.MeshStandardMaterial({
    color: slate.clone().multiplyScalar(0.75),
    roughness: 0.95,
    metalness: 0,
  });
  const podium = new THREE.Mesh(podiumGeo, podiumMat);
  podium.position.y = PODIUM_H / 2;
  podium.castShadow = true;
  podium.receiveShadow = true;
  group.add(podium);

  const n = model.sections;
  for (let i = 0; i < n; i++) {
    const fp = model.towerFootprints[i] ?? model.totalFootprint / n;
    const side = Math.sqrt(fp);
    const towerW = Math.min(side * 1.05, usableW * 0.78);
    const towerD = Math.min(fp / towerW, (usableD / n) * 0.82);

    const slot = n === 1 ? 0 : i / (n - 1) - 0.5; // -0.5 .. 0.5
    const zPos = slot * (usableD - towerD) * 0.92;
    const xJitter = Math.sin(i * 1.7 + 0.3) * (usableW * 0.12);

    const geo = new THREE.BoxGeometry(towerW, model.height, towerD);
    const mat = new THREE.MeshStandardMaterial({
      color: slate,
      roughness: 0.82,
      metalness: 0.05,
      emissive: accent,
      emissiveIntensity: 0.045,
    });
    const tower = new THREE.Mesh(geo, mat);
    tower.position.set(xJitter, PODIUM_H + model.height / 2, zPos);
    tower.castShadow = true;
    tower.receiveShadow = true;
    group.add(tower);

    // cyan accent outline of the tower volume
    const edges = new THREE.EdgesGeometry(geo);
    const line = new THREE.LineSegments(
      edges,
      new THREE.LineBasicMaterial({
        color: accent,
        transparent: true,
        opacity: 0.55,
      }),
    );
    line.position.copy(tower.position);
    group.add(line);

    // floor banding (faint horizontal lines) — cheap visual storeys
    const bandCount = Math.min(model.floors, 30);
    if (bandCount > 1) {
      const bandPts: number[] = [];
      const hw = towerW / 2 + 0.05;
      const hd = towerD / 2 + 0.05;
      for (let b = 1; b < bandCount; b++) {
        const y = PODIUM_H + (model.height * b) / bandCount;
        bandPts.push(-hw, y, -hd, hw, y, -hd);
        bandPts.push(hw, y, -hd, hw, y, hd);
        bandPts.push(hw, y, hd, -hw, y, hd);
        bandPts.push(-hw, y, hd, -hw, y, -hd);
      }
      const bandGeo = new THREE.BufferGeometry();
      bandGeo.setAttribute(
        "position",
        new THREE.Float32BufferAttribute(bandPts, 3),
      );
      const bands = new THREE.LineSegments(
        bandGeo,
        new THREE.LineBasicMaterial({
          color: accent,
          transparent: true,
          opacity: 0.12,
        }),
      );
      bands.position.x = xJitter;
      bands.position.z = zPos;
      group.add(bands);
    }
  }

  return group;
}

// `disposeObject` now lives in ./three-utils (shared with the §7 «Концепция» 3D
// viewer); imported at the top of this module.

// ── live metrics (React-rendered, derived from the same model) ─────────────────

interface Metrics {
  floors: number;
  height: number;
  footprint: number;
  gfa: number;
  farActual: number;
  over: boolean;
}

function modelMetrics(model: MassingModel): Metrics {
  return {
    floors: model.floors,
    height: model.height,
    footprint: model.totalFootprint,
    gfa: model.gfa,
    farActual: model.farActual,
    over: model.farActual > model.maxFar + 0.001,
  };
}

// ── component ─────────────────────────────────────────────────────────────────

export default function MassingScene({
  areaM2,
  maxFar,
  farIsReal = false,
  preview = false,
  onModelChange,
}: MassingSceneProps): React.JSX.Element {
  const parcelArea =
    areaM2 && Number.isFinite(areaM2) && areaM2 > 0 ? areaM2 : DEFAULT_AREA;
  const farTarget =
    maxFar && Number.isFinite(maxFar) && maxFar > 0 ? maxFar : FALLBACK_FAR;

  const [floors, setFloors] = useState(25);
  const [sections, setSections] = useState(2);
  const [hour, setHour] = useState(13);
  const [autoRotate, setAutoRotate] = useState(true);
  const [webglFailed, setWebglFailed] = useState(false);

  const mountRef = useRef<HTMLDivElement | null>(null);
  const rendererRef = useRef<THREE.WebGLRenderer | null>(null);
  const sceneRef = useRef<THREE.Scene | null>(null);
  const cameraRef = useRef<THREE.PerspectiveCamera | null>(null);
  const controlsRef = useRef<OrbitControls | null>(null);
  const sunRef = useRef<THREE.DirectionalLight | null>(null);
  const massGroupRef = useRef<THREE.Group | null>(null);
  const rafRef = useRef<number>(0);
  const resizeObserverRef = useRef<ResizeObserver | null>(null);
  // Keep the latest onModelChange in a ref so the floors/sections effect fires
  // it without taking the callback's identity as a dependency (a new closure each
  // render must not retrigger a mass rebuild / extra recompute).
  const onModelChangeRef = useRef(onModelChange);
  onModelChangeRef.current = onModelChange;

  const model = computeModel(parcelArea, farTarget, floors, sections);
  const metrics = modelMetrics(model);

  // rebuild + reposition the mass whenever floors/sections change.
  const rebuildMass = useCallback((next: MassingModel) => {
    const scene = sceneRef.current;
    const controls = controlsRef.current;
    if (!scene) return;
    if (massGroupRef.current) {
      scene.remove(massGroupRef.current);
      disposeObject(massGroupRef.current);
      massGroupRef.current = null;
    }
    const group = buildMass(next);
    massGroupRef.current = group;
    scene.add(group);
    if (controls) {
      controls.target.set(0, Math.min(next.height * 0.45 + PODIUM_H, 120), 0);
    }
  }, []);

  // place the sun from the current hour.
  const placeSun = useCallback((h: number) => {
    const sun = sunRef.current;
    if (!sun) return;
    const p = sunFromHour(h);
    sun.position.set(p.x, p.y, p.z);
    sun.target.position.set(0, 10, 0);
    const intensity = 0.45 + 0.85 * Math.max(0.05, Math.sin(p.elevation));
    sun.intensity = intensity;
    sun.color.setHSL(
      0.11 - 0.04 * (1 - p.warmth),
      0.55 * p.warmth + 0.12,
      0.62,
    );
  }, []);

  // ── scene lifecycle (mount once) ────────────────────────────────────────────
  useEffect(() => {
    const mount = mountRef.current;
    if (!mount) return;

    // WebGL availability guard.
    let gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;
    try {
      const probe = document.createElement("canvas");
      gl =
        probe.getContext("webgl2") ??
        (probe.getContext("webgl") as WebGLRenderingContext | null);
    } catch {
      gl = null;
    }
    if (!gl) {
      setWebglFailed(true);
      return;
    }

    let width = mount.clientWidth || 600;
    let height = mount.clientHeight || 380;

    let renderer: THREE.WebGLRenderer;
    try {
      renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    } catch {
      setWebglFailed(true);
      return;
    }
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    renderer.setSize(width, height, false);
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.domElement.style.display = "block";
    renderer.domElement.style.width = "100%";
    renderer.domElement.style.height = "100%";
    mount.appendChild(renderer.domElement);
    rendererRef.current = renderer;

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(BG_COLOR);
    scene.fog = new THREE.Fog(BG_COLOR, 220, 520);
    sceneRef.current = scene;

    const camera = new THREE.PerspectiveCamera(42, width / height, 0.5, 2000);
    camera.position.set(120, 110, 140);
    cameraRef.current = camera;

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;
    controls.dampingFactor = 0.08;
    controls.minDistance = 60;
    controls.maxDistance = 420;
    controls.maxPolarAngle = Math.PI / 2 - 0.04;
    controls.target.set(0, 24, 0);
    // Preview mode is hands-off: kill all user interaction so the always-on
    // canvas never steals page scroll / clicks; keep autoRotate driving the
    // turntable (controls.update() in the RAF below keeps it spinning).
    if (preview) {
      controls.enableZoom = false;
      controls.enablePan = false;
      controls.enableRotate = false;
      controls.autoRotate = true;
    } else {
      controls.autoRotate = autoRotate;
    }
    controls.autoRotateSpeed = 0.6;
    controls.update();
    controlsRef.current = controls;

    // ground / context plane
    const ctxGeo = new THREE.PlaneGeometry(600, 600);
    const ctxMat = new THREE.MeshStandardMaterial({
      color: 0x0a1820,
      roughness: 1,
      metalness: 0,
    });
    const ctxPlane = new THREE.Mesh(ctxGeo, ctxMat);
    ctxPlane.rotation.x = -Math.PI / 2;
    ctxPlane.position.y = -0.04;
    ctxPlane.receiveShadow = true;
    scene.add(ctxPlane);

    // parcel plane (slightly lighter than the context)
    const side = Math.sqrt(parcelArea);
    const parcelGeo = new THREE.PlaneGeometry(side, side);
    const parcelMat = new THREE.MeshStandardMaterial({
      color: 0x12222e,
      roughness: 1,
      metalness: 0,
    });
    const parcelPlane = new THREE.Mesh(parcelGeo, parcelMat);
    parcelPlane.rotation.x = -Math.PI / 2;
    parcelPlane.position.y = 0;
    parcelPlane.receiveShadow = true;
    scene.add(parcelPlane);

    // cyan parcel outline
    const hw = side / 2;
    const outlinePts = [
      -hw,
      0.05,
      -hw,
      hw,
      0.05,
      -hw,
      hw,
      0.05,
      hw,
      -hw,
      0.05,
      hw,
      -hw,
      0.05,
      -hw,
    ];
    const outlineGeo = new THREE.BufferGeometry();
    outlineGeo.setAttribute(
      "position",
      new THREE.Float32BufferAttribute(outlinePts, 3),
    );
    const outline = new THREE.Line(
      outlineGeo,
      new THREE.LineBasicMaterial({ color: 0x8ed3ff }),
    );
    scene.add(outline);

    // subtle grid helper
    const grid = new THREE.GridHelper(side, 16, 0x2f6f9f, 0x1c3340);
    grid.position.y = 0.02;
    const gridMat = grid.material as THREE.Material;
    gridMat.opacity = 0.32;
    gridMat.transparent = true;
    scene.add(grid);

    // lighting
    const hemi = new THREE.HemisphereLight(0x9fc4dd, 0x0a141c, 0.55);
    scene.add(hemi);
    const ambient = new THREE.AmbientLight(0xffffff, 0.18);
    scene.add(ambient);

    const sun = new THREE.DirectionalLight(0xfff2d8, 1.15);
    sun.castShadow = true;
    sun.shadow.mapSize.set(2048, 2048);
    sun.shadow.camera.near = 1;
    sun.shadow.camera.far = 500;
    const sExtent = 110;
    sun.shadow.camera.left = -sExtent;
    sun.shadow.camera.right = sExtent;
    sun.shadow.camera.top = sExtent;
    sun.shadow.camera.bottom = -sExtent;
    sun.shadow.bias = -0.0005;
    scene.add(sun);
    scene.add(sun.target);
    sunRef.current = sun;

    // initial sun. The initial mass is built by the dedicated floors/sections
    // effect below, which runs right after this mount effect in the same commit
    // (after paint, like all passive effects) now that sceneRef is set — the RAF
    // render loop paints it on the next frame, so no duplicate build here.
    placeSun(hour);

    // resize handling
    const ro = new ResizeObserver(() => {
      const w = mount.clientWidth;
      const h = mount.clientHeight;
      if (!w || !h) return;
      width = w;
      height = h;
      camera.aspect = w / h;
      camera.updateProjectionMatrix();
      renderer.setSize(w, h, false);
    });
    ro.observe(mount);
    resizeObserverRef.current = ro;

    // render loop
    let alive = true;
    const tick = () => {
      if (!alive) return;
      rafRef.current = requestAnimationFrame(tick);
      controls.update();
      renderer.render(scene, camera);
    };
    rafRef.current = requestAnimationFrame(tick);

    // full teardown on unmount
    return () => {
      alive = false;
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      rafRef.current = 0;

      if (resizeObserverRef.current) {
        resizeObserverRef.current.disconnect();
        resizeObserverRef.current = null;
      }

      controls.dispose();
      controlsRef.current = null;

      disposeObject(scene);
      massGroupRef.current = null;
      sceneRef.current = null;
      cameraRef.current = null;
      sunRef.current = null;

      renderer.dispose();
      const canvas = renderer.domElement;
      if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
      rendererRef.current = null;
    };
    // mount-once effect: parcel area / FAR are stable per-render; slider state is
    // applied through the dedicated effects below so the scene is not rebuilt.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // apply floors / sections changes by rebuilding the mass (no full re-init) and
  // notify the live-economics consumer (Stage 2b) with the recomputed program.
  // Fires on the initial mount build and on every floors/sections change — never
  // on the time-of-day slider (that effect only moves the sun).
  useEffect(() => {
    if (!sceneRef.current) return;
    const next = computeModel(parcelArea, farTarget, floors, sections);
    rebuildMass(next);
    onModelChangeRef.current?.(next);
  }, [floors, sections, parcelArea, farTarget, rebuildMass]);

  // apply time-of-day changes by repositioning the sun.
  useEffect(() => {
    placeSun(hour);
  }, [hour, placeSun]);

  // apply auto-rotate toggle (preview keeps it permanently on — no control).
  useEffect(() => {
    if (preview) return;
    if (controlsRef.current) controlsRef.current.autoRotate = autoRotate;
  }, [autoRotate, preview]);

  if (webglFailed) {
    return (
      <div
        className={
          preview ? styles.massingPreviewViewport : styles.massingViewport
        }
      >
        <div className={styles.massingFallback}>
          <div className={styles.massingFallbackTitle}>
            3D-просмотр недоступен
          </div>
          <p>
            Браузер не поддерживает WebGL — объёмная масса не может быть
            отрисована.
          </p>
        </div>
      </div>
    );
  }

  // Preview: only the autorotating viewport — no sliders / metrics chrome.
  if (preview) {
    return <div ref={mountRef} className={styles.massingPreviewViewport} />;
  }

  return (
    <div className={styles.massingWrap}>
      <div ref={mountRef} className={styles.massingViewport} />

      <div className={styles.massingControls}>
        <div className={styles.massingSliders}>
          <label className={styles.massingSlider}>
            <span className={styles.massingSliderLabel}>
              Этажность
              <span className={styles.massingSliderValue}>{floors}</span>
            </span>
            <input
              type="range"
              min={FLOORS_MIN}
              max={FLOORS_MAX}
              step={1}
              value={floors}
              onChange={(e) => setFloors(Number(e.target.value))}
              aria-label="Этажность"
            />
          </label>

          <label className={styles.massingSlider}>
            <span className={styles.massingSliderLabel}>
              Секций
              <span className={styles.massingSliderValue}>{sections}</span>
            </span>
            <input
              type="range"
              min={SECTIONS_MIN}
              max={SECTIONS_MAX}
              step={1}
              value={sections}
              onChange={(e) => setSections(Number(e.target.value))}
              aria-label="Секций"
            />
          </label>

          <label className={styles.massingSlider}>
            <span className={styles.massingSliderLabel}>
              Время суток
              <span className={styles.massingSliderValue}>{fmtHour(hour)}</span>
            </span>
            <input
              type="range"
              min={HOUR_MIN}
              max={HOUR_MAX}
              step={1}
              value={hour}
              onChange={(e) => setHour(Number(e.target.value))}
              aria-label="Время суток (инсоляция)"
            />
          </label>

          <label className={styles.massingToggle}>
            <input
              type="checkbox"
              checked={autoRotate}
              onChange={(e) => setAutoRotate(e.target.checked)}
            />
            <span>Автоповорот</span>
          </label>
        </div>

        <div className={styles.massingMetrics}>
          <div className={styles.massingMetric}>
            <span className={styles.massingMetricLabel}>Этажей</span>
            <span className={styles.massingMetricValue}>{metrics.floors}</span>
          </div>
          <div className={styles.massingMetric}>
            <span className={styles.massingMetricLabel}>Высота</span>
            <span className={styles.massingMetricValue}>
              {fmtRu(metrics.height)} м
            </span>
          </div>
          <div className={styles.massingMetric}>
            <span className={styles.massingMetricLabel}>Пятно</span>
            <span className={styles.massingMetricValue}>
              {fmtRu(metrics.footprint)} м²
            </span>
          </div>
          <div className={styles.massingMetric}>
            <span className={styles.massingMetricLabel}>GFA</span>
            <span className={styles.massingMetricValue}>
              {fmtRu(metrics.gfa)} м²
            </span>
          </div>
          <div className={styles.massingMetric}>
            <span className={styles.massingMetricLabel}>КСИТ-факт</span>
            <span
              className={`${styles.massingMetricValue} ${
                metrics.over ? styles.massingMetricOver : styles.massingMetricOk
              }`}
            >
              {metrics.farActual.toFixed(2)}
            </span>
          </div>
        </div>

        <div className={styles.massingCaption}>
          КСИТ-цель {farTarget.toFixed(1)} ·{" "}
          {farIsReal ? "регламент НСПД" : "регламент-дефолт, источник НСПД"} ·
          масса генеративная (этажи/секции/время — исследовательские вводные)
        </div>
      </div>
    </div>
  );
}
