import { Vector3 } from "three";
import { Body } from "cannon-es";
import { Character } from "./Character/Character";
import { BaseAnimation } from "./Enums/BaseAnimation";

type PlayerCombatDeps = {
  character: Character;
  getPhysicsBody: () => Body;
  isMoving: () => boolean;
  onStopRunning: () => void;
  onResumeRunning: () => void;
  onSetTurnTarget: (worldPosition: Vector3) => void;
  onUpdateRotation: (delta: number) => void;
  isRotationComplete: () => boolean;
  onSyncRotation: () => void;
};

export class PlayerCombat {
  private static deps: PlayerCombatDeps;

  private static isAttacking = false;
  private static isAutoAttacking = false;
  private static isTurningToTarget = false;
  private static isBatEquipped = false;
  private static isLootLoopActive = false;
  private static pendingCombatExit = false;
  private static attackPhase: "none" | "attacking" = "none";

  private static onStrike: (() => void) | null = null;
  private static onTurnComplete: (() => void) | null = null;
  private static strikeMarkIndex = 0;
  private static prevStrikeNormTime = 0;

  private static readonly turnIdleFade = 0.1;
  private static readonly lootEnterFade = 0.25;
  private static readonly combatExitFade = 0.8;
  /** Normalized clip time (0–1) when the bat crosses the target: R→L, then L→R. */
  private static readonly attackStrikeMarks = [0.38, 0.72];
  /** After 1st hit (R→L) — safe exit if combat ends before 2nd hit (L→R). */
  private static readonly lootExitAfterFirstStrike = 0.5;
  /** After 2nd hit (L→R) — follow-through before idle. */
  private static readonly lootExitAfterSecondStrike = 0.78;
  /** End of clip when exiting before any strike (e.g. left the zone). */
  private static readonly lootExitEndOfCycle = 0.9;

  static init(deps: PlayerCombatDeps) {
    this.deps = deps;
  }

  static get isBusy() {
    return this.isAttacking || this.isTurningToTarget || this.pendingCombatExit;
  }

  static get isAutoAttackActive() {
    return this.isAutoAttacking;
  }

  static startAutoAttack(
    onStrike: () => void,
    _onComplete: () => void,
    targetWorldPosition: Vector3,
  ) {
    if (this.isAutoAttacking) return;

    this.isAutoAttacking = true;
    this.deps.onStopRunning();
    this.beginTurnToTarget(targetWorldPosition, () => this.playAttack(onStrike));
  }

  static retargetAutoAttack(targetWorldPosition: Vector3) {
    if (!this.isAutoAttacking) return;

    this.deps.onSetTurnTarget(targetWorldPosition);
    this.deps.getPhysicsBody().velocity.set(0, 0, 0);

    if (this.isTurningToTarget) {
      return;
    }

    this.onTurnComplete = () => {
      this.deps.onSyncRotation();
    };
    this.isTurningToTarget = true;
  }

  static stopAutoAttack() {
    const wasLooting = this.isLootLoopActive && this.attackPhase === "attacking";
    const needsExit = this.isAutoAttacking
      || this.isTurningToTarget
      || this.isAttacking
      || this.pendingCombatExit
      || this.isBatEquipped;

    this.isAutoAttacking = false;
    this.onStrike = null;
    this.cancelTurnToTarget();

    if (wasLooting) {
      this.pendingCombatExit = true;
      return;
    }

    if (needsExit) {
      this.finishCombatExit();
    }
  }

  static playAttack(onStrike: () => void) {
    this.onStrike = onStrike;
    this.isAttacking = true;

    this.deps.getPhysicsBody().velocity.set(0, 0, 0);

    if (!this.isBatEquipped) {
      this.equipBat();
    }

    this.playBatAttack();
  }

  static update(delta: number): boolean {
    if (this.isTurningToTarget) {
      this.deps.getPhysicsBody().velocity.set(0, 0, 0);
      this.updateTurnToTarget(delta);
      return true;
    }

    if (this.isAttacking) {
      this.deps.getPhysicsBody().velocity.set(0, 0, 0);
      this.updateAttackStrikes();
      return true;
    }

    return false;
  }

  static cancelOnMoveInput() {
    if (this.isAutoAttacking || this.isTurningToTarget) {
      this.stopAutoAttack();
    }
  }

  private static beginTurnToTarget(targetWorldPosition: Vector3, onComplete: () => void) {
    this.deps.onSetTurnTarget(targetWorldPosition);

    this.onTurnComplete = onComplete;
    this.isTurningToTarget = true;
    this.deps.getPhysicsBody().velocity.set(0, 0, 0);
    this.deps.character.playAnimation(BaseAnimation.Idle, false, this.turnIdleFade);
  }

  private static cancelTurnToTarget() {
    if (!this.isTurningToTarget) return;

    this.isTurningToTarget = false;
    this.onTurnComplete = null;
    this.deps.onSyncRotation();
  }

  private static updateTurnToTarget(delta: number) {
    this.deps.onUpdateRotation(delta);

    if (!this.deps.isRotationComplete()) {
      return;
    }

    this.isTurningToTarget = false;
    this.deps.onSyncRotation();

    const callback = this.onTurnComplete;
    this.onTurnComplete = null;
    callback?.();
  }

  private static playBatAttack() {
    if (this.isLootLoopActive && this.isAutoAttacking) return;

    this.startLootLoop();
  }

  private static startLootLoop() {
    const animId = this.getBatAttackAnimation();
    if (animId !== BaseAnimation.Loot) {
      this.attackPhase = "attacking";
      this.deps.character.playAnimation(animId, false, this.lootEnterFade);
      return;
    }

    this.attackPhase = "attacking";
    this.strikeMarkIndex = 0;
    this.prevStrikeNormTime = 0;
    this.isLootLoopActive = true;
    this.deps.character.playAnimation(BaseAnimation.Loot, false, this.lootEnterFade);
  }

  private static updateAttackStrikes() {
    if (this.attackPhase !== "attacking") return;

    const action = this.deps.character.curClipAction;
    if (!action) return;

    const clipDuration = action.getClip().duration;
    if (clipDuration <= 0) return;

    const normalizedTime = action.time / clipDuration;

    if (normalizedTime < this.prevStrikeNormTime) {
      if (this.pendingCombatExit) {
        this.finishCombatExit();
        return;
      }
      this.strikeMarkIndex = 0;
    }
    this.prevStrikeNormTime = normalizedTime;

    while (
      this.strikeMarkIndex < this.attackStrikeMarks.length &&
      normalizedTime >= this.attackStrikeMarks[this.strikeMarkIndex]
    ) {
      this.onStrike?.();
      this.strikeMarkIndex++;
    }

    if (this.pendingCombatExit && this.canExitLootNow(normalizedTime)) {
      this.finishCombatExit();
    }
  }

  private static canExitLootNow(normalizedTime: number) {
    if (this.strikeMarkIndex >= 2) {
      return normalizedTime >= this.lootExitAfterSecondStrike;
    }

    if (this.strikeMarkIndex >= 1) {
      return normalizedTime >= this.lootExitAfterFirstStrike;
    }

    return normalizedTime >= this.lootExitEndOfCycle;
  }

  private static getBatAttackAnimation() {
    if (this.deps.character.animationList.length > BaseAnimation.Loot) {
      return BaseAnimation.Loot;
    }

    return BaseAnimation.Idle;
  }

  private static equipBat() {
    this.isBatEquipped = true;
    this.deps.character.setBatEquipped(true);
  }

  private static finishCombatExit() {
    this.pendingCombatExit = false;
    this.attackPhase = "none";
    this.isAttacking = false;
    this.strikeMarkIndex = 0;
    this.prevStrikeNormTime = 0;
    this.isLootLoopActive = false;
    this.isBatEquipped = false;
    this.deps.character.setDefaultWeapons();
    this.deps.onSyncRotation();

    if (this.deps.isMoving()) {
      this.deps.onResumeRunning();
      return;
    }

    this.deps.character.crossFadeToAnimation(BaseAnimation.Idle, false, this.combatExitFade);
    this.deps.onStopRunning();
  }
}
