import { useRef, useEffect, useState, forwardRef, useImperativeHandle, useCallback } from 'react'
import { useFrame, useThree } from '@react-three/fiber'
import { RigidBody, CapsuleCollider, useRapier } from '@react-three/rapier'
import * as THREE from 'three'
import { useCameraGestures } from '../camera/useCameraGestures'
import { useDebugStore } from '../../store/debug'
import { isEditableTarget } from '../../utils/dom'
import {
  CAMERA_EYE_OFFSET,
  CHARACTER_BODY_SPAWN,
  CHARACTER_BODY_SPAWN_POSITION,
  CHARACTER_HEIGHT,
} from './spawn'

export interface CharacterControllerHandle {
  reset: () => void
}

const SPEED = 4
const JUMP_VELOCITY = 3
const MOUSE_SMOOTHING = 0.12
const DOLLY_UNITS_PER_PIXEL = 0.01
const CHARACTER_RADIUS = 0.25
const CHARACTER_HALF_SEGMENT = CHARACTER_HEIGHT / 2 - CHARACTER_RADIUS
const GROUND_CHECK_DISTANCE = CAMERA_EYE_OFFSET + 0.08
const DEFAULT_YAW = 0

const _forward = new THREE.Vector3()
const _right = new THREE.Vector3()
const _dollyForward = new THREE.Vector3()
const _move = new THREE.Vector3()
const _euler = new THREE.Euler(0, 0, 0, 'YXZ')

export const CharacterController = forwardRef<CharacterControllerHandle>(
  function CharacterController(_, ref) {
  const bodyRef = useRef<React.ComponentRef<typeof RigidBody>>(null)
  const { camera, gl } = useThree()
  const mouseSensitivity = useDebugStore((s) => s.flyMouseSensitivity)
  const { rapier, world } = useRapier()

  const keys = useRef(new Set<string>())
  const jumpQueued = useRef(false)
  const rawYaw = useRef(DEFAULT_YAW)
  const rawPitch = useRef(0)
  const smoothYaw = useRef(DEFAULT_YAW)
  const smoothPitch = useRef(0)
  const touchLook = useRef<{ id: number; x: number; y: number } | null>(null)
  const touchMove = useRef<{ id: number; x: number; y: number } | null>(null)
  const [joystickPos, setJoystickPos] = useState({ x: 0, y: 0 })

  useImperativeHandle(ref, () => ({
    reset: () => {
      if (!bodyRef.current) return

      let spawnX = CHARACTER_BODY_SPAWN.x
      let spawnY = CHARACTER_BODY_SPAWN.y
      let spawnZ = CHARACTER_BODY_SPAWN.z
      let spawnYaw = DEFAULT_YAW
      let spawnPitch = 0

      if (typeof window !== 'undefined') {
        const params = new URLSearchParams(window.location.search)
        const spawnParam = params.get('spawn')
        if (spawnParam) {
          const parts = spawnParam.split(',').map(parseFloat)
          if (parts.length >= 3 && !parts.slice(0, 3).some(isNaN)) {
            spawnX = parts[0]
            spawnY = parts[1] - CAMERA_EYE_OFFSET
            spawnZ = parts[2]
          }
          if (parts.length >= 5 && !parts.slice(3, 5).some(isNaN)) {
            spawnYaw = parts[3]
            spawnPitch = parts[4]
          }
        }
      }

      bodyRef.current.setTranslation({ x: spawnX, y: spawnY, z: spawnZ }, true)
      bodyRef.current.setLinvel({ x: 0, y: 0, z: 0 }, true)
      bodyRef.current.setAngvel({ x: 0, y: 0, z: 0 }, true)
      rawYaw.current = spawnYaw
      rawPitch.current = spawnPitch
      smoothYaw.current = spawnYaw
      smoothPitch.current = spawnPitch
      keys.current.clear()
      camera.quaternion.setFromEuler(_euler.set(spawnPitch, spawnYaw, 0))
    },
  }))

  const applyDolly = useCallback((deltaY: number) => {
    const body = bodyRef.current
    if (!body) return

    _dollyForward.set(0, 0, -1).applyQuaternion(camera.quaternion).setY(0)
    if (_dollyForward.lengthSq() < 0.0001) return
    _dollyForward.normalize()

    const amount = -deltaY * DOLLY_UNITS_PER_PIXEL
    const position = body.translation()
    body.setTranslation(
      {
        x: position.x + _dollyForward.x * amount,
        y: position.y,
        z: position.z + _dollyForward.z * amount,
      },
      true,
    )
    body.wakeUp()
  }, [camera])

  const applyLook = useCallback((dx: number, dy: number) => {
    rawYaw.current -= dx * mouseSensitivity
    rawPitch.current -= dy * mouseSensitivity
    rawPitch.current = Math.max(-Math.PI / 2.2, Math.min(Math.PI / 2.2, rawPitch.current))
  }, [mouseSensitivity])

  useCameraGestures({ domElement: gl.domElement, onDollyPixels: applyDolly, onTumblePixels: applyLook })

  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (isEditableTarget(e.target)) {
        keys.current.delete(e.code)
        return
      }
      if (e.type === 'keydown') {
        keys.current.add(e.code)
        if (e.code === 'Space' && !e.repeat) {
          jumpQueued.current = true
          e.preventDefault()
        }
      } else {
        keys.current.delete(e.code)
      }
    }
    window.addEventListener('keydown', onKey)
    window.addEventListener('keyup', onKey)

    // Touch handlers
    const onTouchStart = (e: TouchEvent) => {
      if (e.touches.length >= 2) return
      for (const touch of Array.from(e.changedTouches)) {
        const isLeft = touch.clientX < window.innerWidth / 2
        if (isLeft && !touchMove.current) {
          touchMove.current = { id: touch.identifier, x: touch.clientX, y: touch.clientY }
          setJoystickPos({ x: touch.clientX, y: touch.clientY })
        } else if (!isLeft && !touchLook.current) {
          touchLook.current = { id: touch.identifier, x: touch.clientX, y: touch.clientY }
        }
      }
    }
    const onTouchMove = (e: TouchEvent) => {
      if (e.touches.length >= 2) return
      for (const touch of Array.from(e.changedTouches)) {
        if (touchLook.current?.id === touch.identifier) {
          const dx = touch.clientX - touchLook.current.x
          const dy = touch.clientY - touchLook.current.y
          rawYaw.current -= dx * 0.004
          rawPitch.current -= dy * 0.004
          rawPitch.current = Math.max(-Math.PI / 2.2, Math.min(Math.PI / 2.2, rawPitch.current))
          touchLook.current = { id: touch.identifier, x: touch.clientX, y: touch.clientY }
        }
        if (touchMove.current?.id === touch.identifier) {
          touchMove.current = { ...touchMove.current, x: touch.clientX, y: touch.clientY }
        }
      }
    }
    const onTouchEnd = (e: TouchEvent) => {
      for (const touch of Array.from(e.changedTouches)) {
        if (touchLook.current?.id === touch.identifier) touchLook.current = null
        if (touchMove.current?.id === touch.identifier) {
          touchMove.current = null
          setJoystickPos({ x: 0, y: 0 })
        }
      }
    }
    gl.domElement.addEventListener('touchstart', onTouchStart, { passive: true })
    gl.domElement.addEventListener('touchmove', onTouchMove, { passive: true })
    gl.domElement.addEventListener('touchend', onTouchEnd, { passive: true })

    return () => {
      window.removeEventListener('keydown', onKey)
      window.removeEventListener('keyup', onKey)
      gl.domElement.removeEventListener('touchstart', onTouchStart)
      gl.domElement.removeEventListener('touchmove', onTouchMove)
      gl.domElement.removeEventListener('touchend', onTouchEnd)
    }
  }, [gl.domElement])

  useFrame((_state, delta) => {
    const body = bodyRef.current
    if (!body) return

    const smoothing = 1 - Math.pow(1 - MOUSE_SMOOTHING, delta * 60)
    smoothYaw.current += (rawYaw.current - smoothYaw.current) * smoothing
    smoothPitch.current += (rawPitch.current - smoothPitch.current) * smoothing

    _euler.set(smoothPitch.current, smoothYaw.current, 0)
    camera.quaternion.setFromEuler(_euler)

    // Compute move direction from keys + touch joystick
    let fwd = 0, strafe = 0
    if (keys.current.has('KeyW') || keys.current.has('ArrowUp')) fwd += 1
    if (keys.current.has('KeyS') || keys.current.has('ArrowDown')) fwd -= 1
    if (keys.current.has('KeyA') || keys.current.has('ArrowLeft')) strafe -= 1
    if (keys.current.has('KeyD') || keys.current.has('ArrowRight')) strafe += 1

    if (touchMove.current) {
      const origin = joystickPos
      const dx = (touchMove.current.x - origin.x) / 50
      const dy = (touchMove.current.y - origin.y) / 50
      strafe += Math.max(-1, Math.min(1, dx))
      fwd -= Math.max(-1, Math.min(1, dy))
    }

    _forward.set(0, 0, -1).applyQuaternion(camera.quaternion).setY(0).normalize()
    _right.set(1, 0, 0).applyQuaternion(camera.quaternion).setY(0).normalize()
    _move.set(0, 0, 0).addScaledVector(_forward, fwd).addScaledVector(_right, strafe)
    if (_move.lengthSq() > 1) _move.normalize()

    const vel = body.linvel()
    let nextY = vel.y
    if (jumpQueued.current) {
      const pos = body.translation()
      const ray = new rapier.Ray(
        { x: pos.x, y: pos.y, z: pos.z },
        { x: 0, y: -1, z: 0 },
      )
      const grounded = Boolean(world.castRay(ray, GROUND_CHECK_DISTANCE, true, undefined, undefined, undefined, body))
      if (grounded) nextY = JUMP_VELOCITY
      jumpQueued.current = false
    }
    body.setLinvel({ x: _move.x * SPEED, y: nextY, z: _move.z * SPEED }, true)

    // Sync camera position to body
    const pos = body.translation()
    camera.position.set(pos.x, pos.y + CAMERA_EYE_OFFSET, pos.z)
  })

  return (
    <RigidBody
      ref={bodyRef}
      position={CHARACTER_BODY_SPAWN_POSITION}
      enabledRotations={[false, false, false]}
      linearDamping={0}
    >
      <CapsuleCollider args={[CHARACTER_HALF_SEGMENT, CHARACTER_RADIUS]} />
    </RigidBody>
  )
})
