import { useState, useCallback, useEffect, useMemo, useRef, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { PerspectiveCamera, Environment } from '@react-three/drei';
import * as THREE from 'three';
import { useEditor } from '../context/EditorContext';
import { useTheme } from '../../../contexts/ThemeContext';
import { boundingBox, polygonCentroid } from '../utils/geometry';
import { FloorCeiling } from './FloorCeiling';
import { WallMesh } from './WallMesh';
import { DoorOpening } from './DoorOpening';
import { WindowOpening } from './WindowOpening';
import { FurnitureMesh } from './FurnitureMesh';
import { ElectricalMeshWithHeight } from './ElectricalMesh';
import { PlinthMesh } from './PlinthMesh';
import { RoomLabels } from './RoomLabels';
import {
  SceneCamera,
  CameraPresetsUI,
  CameraOrientationTracker,
  type CameraPreset,
  type CameraOrientation,
} from './CameraControls';

import type { Wall } from '@house-plan-maker/shared';

/** Determines the two walls closest to the camera and reports their IDs. */
function NearestWallTracker({ walls, onUpdate }: { readonly walls: readonly Wall[]; readonly onUpdate: (ids: ReadonlySet<string>) => void }) {
  const { camera } = useThree();
  const lastIdsRef = useRef<string>('');
  const frameCountRef = useRef(0);

  useFrame(() => {
    // Only check every 10 frames for performance
    frameCountRef.current++;
    if (frameCountRef.current % 10 !== 0) return;

    const camPos = camera.position;
    const camDir = new THREE.Vector3();
    camera.getWorldDirection(camDir);

    // Sort walls by distance to camera
    const sorted = walls
      .map((wall) => {
        const midX = (wall.startX + wall.endX) / 2;
        const midZ = (wall.startY + wall.endY) / 2;
        const dist = Math.sqrt((camPos.x - midX) ** 2 + (camPos.z - midZ) ** 2);
        return { id: wall.id, dist };
      })
      .sort((a, b) => a.dist - b.dist);

    // Don't hide when in bird's eye (camera high above)
    const camHeight = camPos.y;
    const closestDist = sorted[0]?.dist ?? 0;
    const hide = closestDist > 0.5 && camHeight < closestDist * 2;

    const ids = hide
      ? new Set(sorted.slice(0, 2).map((s) => s.id))
      : new Set<string>();

    const key = [...ids].sort().join(',');
    if (key !== lastIdsRef.current) {
      lastIdsRef.current = key;
      onUpdate(ids);
    }
  });

  return null;
}

interface Room3DViewProps {
  /**
   * Optional mutable ref — set by the room editor so external components
   * (notably the export dialog) can imperatively snap the camera to a preset
   * (e.g. 'birds-eye') before taking a screenshot.
   */
  readonly presetTriggerRef?: React.MutableRefObject<((preset: CameraPreset) => void) | null>;
  /**
   * Optional ref for the underlying R3F canvas. Forwarded so the export
   * dialog can always reach the live canvas even when Room3DView was lazy-
   * mounted mid-export (querying the outer container at mount time would
   * miss a canvas that hadn't been created yet).
   */
  readonly canvasRef?: React.MutableRefObject<HTMLCanvasElement | null>;
}

/**
 * Room3DView — read-only 3D perspective view of the room.
 * Renders inside a @react-three/fiber Canvas with orbit controls.
 */
export function Room3DView({ presetTriggerRef, canvasRef }: Room3DViewProps = {}) {
  const { t, i18n } = useTranslation();
  const { state, dispatch } = useEditor();
  const { theme } = useTheme();
  const { room, walls, openings, electricalItems, furnitureItems, selectedIds, layerVisibility, globalFurnitureOpacity } = state;
  // Canvas backdrop tracks the app theme: a soft slate-grey for light mode
  // and a deep neutral for dark mode so the white walls of the room don't
  // bleed into the page background.
  const canvasBackground = theme === 'dark' ? '#1a1d23' : '#e8ecf0';

  // Global furniture opacity is applied per-item inside FurnitureMesh via
  // `effectiveOpacity = item.opacity * globalOpacity` on cloned materials.
  // No scene-level mutation is needed any more — it's just a prop.

  const [activePreset, setActivePreset] = useState<CameraPreset | null>(null);
  const [hiddenWallIds, setHiddenWallIds] = useState<ReadonlySet<string>>(new Set());
  const [cameraOrientation, setCameraOrientation] = useState<CameraOrientation>({
    yawDeg: -32,
    pitchDeg: 26,
  });

  // Sun (key directional light) position. Stored as spherical coordinates so
  // sliders are intuitive: azimuth rotates the sun around the room, elevation
  // raises/lowers it above the horizon. The cartesian position is recomputed
  // from these on every change. Viewer-only state — not persisted to the room.
  const [sunAzimuthDeg, setSunAzimuthDeg] = useState(45);
  const [sunElevationDeg, setSunElevationDeg] = useState(55);
  // Lowered default sun intensity (was 0.7) so the directional key light no
  // longer dominates over the omnidirectional contribution, which is what
  // caused walls facing away from the sun to look noticeably darker. The
  // slider still lets the user crank it back up for stronger shadow drama.
  const [sunIntensity, setSunIntensity] = useState(0.35);
  const [showLightControls, setShowLightControls] = useState(false);

  // Whether door panels render ajar (true) or closed (false). Viewer-only
  // setting; not persisted because it's purely a visualisation preference.
  const [doorsOpen, setDoorsOpen] = useState(true);

  const sunPosition = useMemo<[number, number, number]>(() => {
    const azimuth = (sunAzimuthDeg * Math.PI) / 180;
    const elevation = (sunElevationDeg * Math.PI) / 180;
    const radius = 18; // far enough to act like a distant sun
    const horizontal = Math.cos(elevation) * radius;
    return [
      Math.cos(azimuth) * horizontal,
      Math.sin(elevation) * radius,
      Math.sin(azimuth) * horizontal,
    ];
  }, [sunAzimuthDeg, sunElevationDeg]);

  const handlePreset = useCallback((preset: CameraPreset) => {
    setActivePreset(preset);
    // Reset after applying so it can be re-applied
    setTimeout(() => setActivePreset(null), 100);
  }, []);

  // Expose handlePreset so the export dialog can snap the camera to 'birds-eye'
  // before capturing the 3D canvas for PDF export.
  useEffect(() => {
    if (!presetTriggerRef) return;
    presetTriggerRef.current = handlePreset;
    return () => {
      presetTriggerRef.current = null;
    };
  }, [presetTriggerRef, handlePreset]);

  // Reset the camera to the default (Bird's Eye) view. Bound to the Canvas
  // element's onDoubleClick so double-tapping empty space snaps back.
  const handleResetView = useCallback(() => {
    handlePreset('birds-eye');
  }, [handlePreset]);

  const handleSelect = useCallback(
    (id: string) => {
      dispatch({ type: 'SET_SELECTED', ids: new Set([id]) });
    },
    [dispatch],
  );

  const wallHeight = room.wallHeight;
  const plinthHeight = room.plinthHeight;
  const plinthThickness = room.plinthThickness;
  const shape = room.shape;

  // Separate openings by type
  const doors = useMemo(
    () => openings.filter((o) => o.type === 'DOOR'),
    [openings],
  );
  const windows = useMemo(
    () => openings.filter((o) => o.type === 'WINDOW'),
    [openings],
  );

  // Centroid of the room polygon — used to push wall + plinth meshes outward
  // so they don't bleed into the room interior. See WallMesh for the why.
  const roomCentroid = useMemo(
    () => (shape.length >= 3 ? polygonCentroid(shape) : { x: 0, y: 0 }),
    [shape],
  );

  // Compute bird's eye camera position from room bounds
  const initialCameraPos = useMemo((): [number, number, number] => {
    if (shape.length < 3) return [0, 10, 0.01];
    const bb = boundingBox(shape);
    const cx = (bb.minX + bb.maxX) / 2;
    const cz = (bb.minY + bb.maxY) / 2;
    const maxSize = Math.max(bb.maxX - bb.minX, bb.maxY - bb.minY, 2);
    return [cx, maxSize * 1.5 * 1.5, cz + 0.01];
  }, [shape]);

  // Wall lookup by ID for openings
  const wallMap = useMemo(() => {
    const map = new Map<string, (typeof walls)[number]>();
    for (const w of walls) {
      map.set(w.id, w);
    }
    return map;
  }, [walls]);

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      <CameraPresetsUI
        shape={shape}
        wallHeight={wallHeight}
        orientation={cameraOrientation}
        onPreset={handlePreset}
      />
      <Canvas
        ref={(el) => {
          if (canvasRef) canvasRef.current = el ?? null;
        }}
        // `shadows="percentage"` selects PCFShadowMap — the non-deprecated
        // replacement for the default PCFSoftShadowMap that Three.js removed
        // support for in recent versions. Double-click anywhere on the empty
        // canvas background resets the camera to the default Bird's Eye view.
        shadows="percentage"
        // Render at the device pixel ratio (capped at 2) for crisp output on
        // HiDPI displays without blowing up GPU cost on 3x screens.
        dpr={[1, 2]}
        onDoubleClick={handleResetView}
        style={{ width: '100%', height: '100%', background: canvasBackground }}
        gl={{
          antialias: true,
          preserveDrawingBuffer: true,
          powerPreference: 'high-performance',
          // ACES filmic tone mapping gives more natural highlights/shadows
          // than the default LinearToneMapping. Exposure is tuned low enough
          // that the IBL environment + directional lights don't blow out
          // light walls and floors.
          toneMapping: THREE.ACESFilmicToneMapping,
          toneMappingExposure: 0.75,
          outputColorSpace: THREE.SRGBColorSpace,
        }}
      >
        <Suspense fallback={null}>
          {/* Camera + Controls */}
          <PerspectiveCamera makeDefault fov={50} near={0.1} far={200} position={initialCameraPos} />
          <SceneCamera
            shape={shape}
            wallHeight={wallHeight}
            activePreset={activePreset}
          />
          <CameraOrientationTracker onChange={setCameraOrientation} />

          {/* Image-based lighting from a pre-baked HDR environment is now
              the PRIMARY light source. IBL integrates light from every
              direction so walls facing any orientation receive roughly
              equal illumination — that's what we want for aligned wall
              brightness. `environmentIntensity` was bumped from 0.35 to
              0.85 to dominate over the weaker directional lights.
              `background={false}` keeps the canvas grey. */}
          <Environment preset="apartment" background={false} environmentIntensity={0.85} />

          {/* Analytic lighting layered on top of the IBL.
              - Ambient light: a flat minimum brightness floor so even
                walls with no direct light never fall below a usable level.
              - Hemisphere light: subtle sky/ground tint adds a natural
                gradient from floor to ceiling without favouring any wall.
              - Key directional light (sun): intensity lowered to 0.35 in
                the default slider state so it adds shadow contrast
                without visibly overpowering any single wall.
              - Fill light removed — the omnidirectional IBL + ambient +
                hemisphere combination already lights the back walls. */}
          <ambientLight intensity={0.3} />
          <hemisphereLight args={['#f0f4f8', '#3a2f1e', 0.35]} />
          <directionalLight
            position={sunPosition}
            intensity={sunIntensity}
            castShadow
            shadow-mapSize-width={4096}
            shadow-mapSize-height={4096}
            shadow-camera-near={0.5}
            shadow-camera-far={50}
            shadow-camera-left={-15}
            shadow-camera-right={15}
            shadow-camera-top={15}
            shadow-camera-bottom={-15}
            shadow-bias={-0.0001}
            shadow-normalBias={0.02}
            shadow-radius={4}
          />

          {/* Track nearest wall to camera and hide it */}
          <NearestWallTracker walls={walls} onUpdate={setHiddenWallIds} />

          {/* Floor */}
          <FloorCeiling
            shape={shape}
            wallHeight={wallHeight}
            floorType={room.floorType}
            stretchCeilingOffset={
              layerVisibility.stretchCeiling ? room.stretchCeilingOffset : 0
            }
          />

          {/* Walls (hide the one facing the camera) */}
          {layerVisibility.walls && walls.map((wall) => (
            hiddenWallIds.has(wall.id) ? null : (
              <WallMesh
                key={wall.id}
                wall={wall}
                openings={openings}
                wallHeight={wallHeight}
                wallColor={room.wallColor}
                wallFinish={room.wallFinish}
                selectedIds={selectedIds}
                onSelect={handleSelect}
                roomCentroid={roomCentroid}
              />
            )
          ))}

          {/* Plinths (hide matching wall's plinth too) */}
          {layerVisibility.walls && walls.map((wall) => (
            hiddenWallIds.has(wall.id) ? null : (
              <PlinthMesh
                key={`plinth-${wall.id}`}
                wall={wall}
                openings={openings}
                plinthHeight={plinthHeight}
                plinthThickness={plinthThickness}
                roomCentroid={roomCentroid}
              />
            )
          ))}

          {/* Door openings */}
          {doors.map((door) => {
            const wall = wallMap.get(door.wallId);
            if (!wall) return null;
            return (
              <DoorOpening
                key={door.id}
                opening={door}
                wall={wall}
                isSelected={selectedIds.has(door.id)}
                onSelect={handleSelect}
                isOpen={doorsOpen}
              />
            );
          })}

          {/* Window openings */}
          {windows.map((win) => {
            const wall = wallMap.get(win.wallId);
            if (!wall) return null;
            return (
              <WindowOpening
                key={win.id}
                opening={win}
                wall={wall}
                isSelected={selectedIds.has(win.id)}
                onSelect={handleSelect}
              />
            );
          })}

          {/* Furniture */}
          {layerVisibility.furniture && furnitureItems.map((item) => (
            <FurnitureMesh
              key={item.id}
              item={item}
              isSelected={selectedIds.has(item.id)}
              onSelect={handleSelect}
              globalOpacity={globalFurnitureOpacity}
            />
          ))}

          {/* Electrical */}
          {layerVisibility.electrical && electricalItems.map((item) => (
            <ElectricalMeshWithHeight
              key={item.id}
              item={item}
              wallMap={wallMap}
              wallHeight={wallHeight}
              isSelected={selectedIds.has(item.id)}
              onSelect={handleSelect}
              outletWidth={room.outletWidth}
              outletHeight={room.outletHeight}
              roomCentroid={roomCentroid}
            />
          ))}

          {/* Room labels and dimensions */}
          <RoomLabels
            roomName={room.name}
            shape={shape}
            walls={walls}
            wallHeight={wallHeight}
          />

          {/* Contact shadows on the floor */}
          {/* ContactShadows removed — floor is handled by FloorCeiling */}
        </Suspense>
      </Canvas>

      {/* Sun/light controls — collapsible HUD anchored bottom-right of the
          3D viewport. Sliders drive azimuth (rotation around the room),
          elevation (height above the horizon), and the directional light
          intensity. State is local to this component, not persisted. */}
      <div
        style={{
          position: 'absolute',
          right: 12,
          bottom: 12,
          background: 'rgba(255, 255, 255, 0.92)',
          borderRadius: 8,
          padding: showLightControls ? '10px 12px' : '6px 10px',
          boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
          fontSize: 12,
          minWidth: showLightControls ? 200 : undefined,
          color: '#333',
        }}
      >
        <button
          onClick={() => setShowLightControls((v) => !v)}
          style={{
            background: 'none',
            border: 'none',
            cursor: 'pointer',
            padding: 0,
            font: 'inherit',
            color: 'inherit',
            display: 'flex',
            alignItems: 'center',
            gap: 6,
            width: '100%',
          }}
          aria-expanded={showLightControls}
        >
          <span aria-hidden style={{ fontSize: 14 }}>{'\u2600'}</span>
          <span style={{ flex: 1, textAlign: 'left' }}>
            {i18n.exists('view3d.lightControls') ? t('view3d.lightControls') : 'Light'}
          </span>
          <span aria-hidden>{showLightControls ? '\u25BE' : '\u25B8'}</span>
        </button>
        {showLightControls && (
          <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
            <SliderRow
              label={i18n.exists('view3d.azimuth') ? t('view3d.azimuth') : 'Azimuth'}
              value={sunAzimuthDeg}
              min={0}
              max={360}
              step={5}
              suffix={'\u00b0'}
              onChange={setSunAzimuthDeg}
            />
            <SliderRow
              label={i18n.exists('view3d.elevation') ? t('view3d.elevation') : 'Elevation'}
              value={sunElevationDeg}
              min={5}
              max={89}
              step={1}
              suffix={'\u00b0'}
              onChange={setSunElevationDeg}
            />
            <SliderRow
              label={i18n.exists('view3d.intensity') ? t('view3d.intensity') : 'Intensity'}
              value={sunIntensity}
              min={0}
              max={2}
              step={0.05}
              suffix=""
              format={(v) => v.toFixed(2)}
              onChange={setSunIntensity}
            />
            <button
              onClick={() => {
                setSunAzimuthDeg(45);
                setSunElevationDeg(55);
                setSunIntensity(0.35);
              }}
              style={{
                marginTop: 4,
                background: 'transparent',
                border: '1px solid var(--color-border, #ccc)',
                borderRadius: 4,
                padding: '3px 6px',
                cursor: 'pointer',
                font: 'inherit',
                color: 'inherit',
              }}
            >
              {i18n.exists('view3d.reset') ? t('view3d.reset') : 'Reset'}
            </button>
            <hr style={{ border: 'none', borderTop: '1px solid var(--color-border, #e2e2e2)', margin: '4px 0' }} />
            {/* Door display toggle — purely a viewer preference. */}
            <label style={{ display: 'flex', alignItems: 'center', gap: 6, cursor: 'pointer', fontSize: 11, color: '#555' }}>
              <input
                type="checkbox"
                checked={doorsOpen}
                onChange={(e) => setDoorsOpen(e.target.checked)}
              />
              <span>{i18n.exists('view3d.doorsOpen') ? t('view3d.doorsOpen') : 'Show doors open'}</span>
            </label>
          </div>
        )}
      </div>
    </div>
  );
}

interface SliderRowProps {
  readonly label: string;
  readonly value: number;
  readonly min: number;
  readonly max: number;
  readonly step: number;
  readonly suffix: string;
  readonly onChange: (value: number) => void;
  readonly format?: (value: number) => string;
}

function SliderRow({ label, value, min, max, step, suffix, onChange, format }: SliderRowProps) {
  const display = format ? format(value) : Math.round(value).toString();
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
      <span style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#555' }}>
        <span>{label}</span>
        <span style={{ fontVariantNumeric: 'tabular-nums' }}>{display}{suffix}</span>
      </span>
      <input
        type="range"
        min={min}
        max={max}
        step={step}
        value={value}
        onChange={(e) => {
          const next = parseFloat(e.target.value);
          if (Number.isFinite(next)) onChange(next);
        }}
        style={{ width: '100%' }}
      />
    </label>
  );
}
