import { log } from '../debugLog'
import type { CalibrationStatus, CalibratedBaselines, Landmark2D, Landmark3D, Point2D } from './types'


const CALIBRATION_MS = 5000
const FRAMING_MARGIN = 0.15
const GAMEPLAY_MARGIN = 0.10

function landmarkVisible(landmarks: Landmark2D[], idx: number): boolean {
  const lm = landmarks[idx]
  return !!lm && (lm.visibility === undefined || lm.visibility >= 0.5)
}

interface CalibrationSamples {
  ankleL: number[]
  ankleR: number[]
  footSpread: number[]
  heelCalib: { lx: number; lz: number; rx: number; rz: number }[]
}

export class CalibrationMachine {
  private _status: CalibrationStatus = 'framing'
  private _framingHint: string | null = null
  private _baselines: CalibratedBaselines | null = null
  private _feetVisibleSince: number | null = null
  private _samples: CalibrationSamples = { ankleL: [], ankleR: [], footSpread: [], heelCalib: [] }

  get status(): CalibrationStatus { return this._status }
  get framingHint(): string | null { return this._framingHint }
  get baselines(): CalibratedBaselines | null { return this._baselines }

  reset(): void {
    this._status = 'framing'
    this._framingHint = null
    this._feetVisibleSince = null
    this._samples = { ankleL: [], ankleR: [], footSpread: [], heelCalib: [] }
  }

  /**
   * Call each frame. Returns true if calibration status changed.
   * Does NOT handle 'ready' state — that's the engine's domain.
   */
  update(landmarks2D: Landmark2D[], worldLandmarks: Landmark3D[], now: number): boolean {
    const prev = this._status

    if (!landmarks2D || landmarks2D.length < 33) {
      return this.handleLostLandmarks()
    }

    // Check bounds (except during framing itself)
    if (this._status !== 'framing') {
      const oob = this.checkOutOfBounds(landmarks2D)
      if (oob) return prev !== this._status
    }

    switch (this._status) {
      case 'framing': this.updateFraming(landmarks2D); break
      case 'paused': this.updatePaused(landmarks2D); break
      case 'waiting': this.updateWaiting(landmarks2D, now); break
      case 'calibrating': this.updateCalibrating(landmarks2D, worldLandmarks, now); break
      case 'pos-center': this.updatePosCenter(); break
      case 'ready': break // engine handles this
    }

    return prev !== this._status
  }

  /** Transition to paused (called by engine when pause gesture detected during play). */
  pause(): void {
    this._status = 'paused'
    log(`[calibration] paused`)
  }

  /** Transition back to ready (called by engine when pause gesture released). */
  resume(): void {
    if (this._status === 'paused') {
      this._status = 'ready'
      this._framingHint = null
      log(`[calibration] resumed from pause`)
    }
  }

  /** Transition to ready (called by engine after crossed-arms gesture in pos-center). */
  confirmReady(): void {
    this._status = 'ready'
  }

  private handleLostLandmarks(): boolean {
    if (this._status === 'calibrating') {
      this.reset()
      log(`[calibration] lost landmarks, reset to framing`)
      return true
    }
    if (this._status === 'ready') {
      this._status = 'paused'
      log(`[calibration] lost landmarks during play, paused`)
      return true
    }
    return false
  }

  private checkOutOfBounds(landmarks2D: Landmark2D[]): boolean {
    const nose = landmarks2D[0]
    const noseVis = nose && (nose.visibility === undefined || nose.visibility >= 0.5)
    const toesVis = landmarkVisible(landmarks2D, 31) && landmarkVisible(landmarks2D, 32)
    const noseY = nose?.y ?? 0
    const toeMaxY = Math.max(landmarks2D[31]?.y ?? 1, landmarks2D[32]?.y ?? 1)
    const inBounds = noseVis && toesVis && noseY > 0 && toeMaxY < (1 - GAMEPLAY_MARGIN)

    if (!inBounds) {
      if (this._status === 'calibrating') {
        this.reset()
        log(`[calibration] left red zone, reset to framing`)
      } else if (this._status === 'ready') {
        this._status = 'paused'
        log(`[calibration] left red zone during play, paused`)
      }
      return true
    }
    return false
  }

  private updateFraming(landmarks2D: Landmark2D[]): void {
    const nose = landmarks2D[0]
    const noseVis = nose && (nose.visibility === undefined || nose.visibility >= 0.5)
    const anklesVis = landmarkVisible(landmarks2D, 27) && landmarkVisible(landmarks2D, 28)
    const toesVis = landmarkVisible(landmarks2D, 31) && landmarkVisible(landmarks2D, 32)

    const noseY = nose?.y ?? 0
    const toeMaxY = Math.max(landmarks2D[31]?.y ?? 1, landmarks2D[32]?.y ?? 1)
    const topOk = noseVis && noseY > 0
    const bottomOk = toesVis && toeMaxY < (1 - FRAMING_MARGIN)

    if (topOk && bottomOk && anklesVis) {
      this._status = 'waiting'
      this._framingHint = null
      log(`[calibration] in frame — noseY=${noseY.toFixed(3)} toeY=${toeMaxY.toFixed(3)}`)
    } else {
      if (!topOk) this._framingHint = 'head not visible'
      else if (!bottomOk || !anklesVis) this._framingHint = 'feet are outside the camera view'
      else this._framingHint = null
    }
  }

  private updatePaused(landmarks2D: Landmark2D[]): void {
    const nose = landmarks2D[0]
    const noseVis = nose && (nose.visibility === undefined || nose.visibility >= 0.5)
    const anklesVis = landmarkVisible(landmarks2D, 27) && landmarkVisible(landmarks2D, 28)
    const toesVis = landmarkVisible(landmarks2D, 31) && landmarkVisible(landmarks2D, 32)
    const noseY = nose?.y ?? 0
    const toeMaxY = Math.max(landmarks2D[31]?.y ?? 1, landmarks2D[32]?.y ?? 1)

    const inBounds = noseVis && toesVis && anklesVis
      && noseY > 0 && toeMaxY < (1 - FRAMING_MARGIN)

    if (!inBounds) {
      if (!noseVis || noseY <= 0) this._framingHint = 'head not visible'
      else this._framingHint = 'feet are outside the camera view'
    } else {
      this._framingHint = null
    }
  }

  private updateWaiting(landmarks2D: Landmark2D[], now: number): void {
    const bothVisible = landmarkVisible(landmarks2D, 27) && landmarkVisible(landmarks2D, 28)
    if (!bothVisible) {
      this.reset()
      log(`[calibration] lost ankles, reset to framing`)
      return
    }

    this._feetVisibleSince = now
    this._status = 'calibrating'
    this._samples = {
      ankleL: [landmarks2D[27].y],
      ankleR: [landmarks2D[28].y],
      footSpread: [],
      heelCalib: [],
    }
    log(`[calibration] both ankles visible, starting calibration`)
  }

  private updateCalibrating(landmarks2D: Landmark2D[], worldLandmarks: Landmark3D[], now: number): void {
    const bothVisible = landmarkVisible(landmarks2D, 27) && landmarkVisible(landmarks2D, 28)
    if (!bothVisible) {
      this.reset()
      log(`[calibration] lost ankles, reset calibration`)
      return
    }

    this._samples.ankleL.push(landmarks2D[27].y)
    this._samples.ankleR.push(landmarks2D[28].y)

    if (worldLandmarks && worldLandmarks.length >= 33) {
      const lh = worldLandmarks[29], rh = worldLandmarks[30]
      const dx = lh.x - rh.x, dz = lh.z - rh.z
      this._samples.footSpread.push(Math.sqrt(dx * dx + dz * dz))

      const lt = worldLandmarks[31], rt = worldLandmarks[32]
      this._samples.heelCalib.push({ lx: lt.x, lz: lt.z, rx: rt.x, rz: rt.z })
    }

    const elapsed = now - this._feetVisibleSince!
    if (elapsed >= CALIBRATION_MS) {
      this.finalizeCalibration()
    }
  }

  private updatePosCenter(): void {
    // Engine calls confirmReady() when crossed-arms gesture is detected
  }

  private finalizeCalibration(): void {
    const avg = (arr: number[]) => arr.reduce((a, b) => a + b, 0) / arr.length

    const leftY = avg(this._samples.ankleL)
    const rightY = avg(this._samples.ankleR)

    let footSpread = 0.35
    if (this._samples.footSpread.length > 0) {
      footSpread = avg(this._samples.footSpread) + 0.10
    }

    let heelBaseL: Point2D = { x: 0, z: 0 }
    let heelBaseR: Point2D = { x: 0, z: 0 }
    if (this._samples.heelCalib.length > 0) {
      const n = this._samples.heelCalib.length
      const sum = this._samples.heelCalib.reduce((a, s) => ({
        lx: a.lx + s.lx, lz: a.lz + s.lz, rx: a.rx + s.rx, rz: a.rz + s.rz,
      }), { lx: 0, lz: 0, rx: 0, rz: 0 })
      heelBaseL = { x: sum.lx / n, z: sum.lz / n }
      heelBaseR = { x: sum.rx / n, z: sum.rz / n }
      log(`[calibration] heel baseline (avg ${n} samples): L=(${heelBaseL.x.toFixed(3)},${heelBaseL.z.toFixed(3)}) R=(${heelBaseR.x.toFixed(3)},${heelBaseR.z.toFixed(3)})`)
    }

    this._baselines = { leftY, rightY, heelBaseL, heelBaseR, footSpread }
    this._status = 'pos-center'
    log(`[calibration] calibration done, footSpread=${footSpread.toFixed(3)}m`)
  }
}
