#include "ZMMOPlayerCharacter.h"

#include "Animation/AnimInstance.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "Engine/GameInstance.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/World.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputAction.h"
#include "InputActionValue.h"
#include "GameFramework/PlayerState.h"
#include "Subsystems/WorldSubsystem.h"
#include "UObject/ConstructorHelpers.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h"
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"

DEFINE_LOG_CATEGORY(LogZMMOPlayer);

AZMMOPlayerCharacter::AZMMOPlayerCharacter()
{
	PrimaryActorTick.bCanEverTick = true;
	PrimaryActorTick.bStartWithTickEnabled = true;

	GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);

	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	UCharacterMovementComponent* CMC = GetCharacterMovement();
	CMC->bOrientRotationToMovement = true;
	CMC->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
	CMC->JumpZVelocity = 500.0f;
	CMC->AirControl = 0.35f;
	CMC->MaxWalkSpeed = 500.0f;
	CMC->MinAnalogWalkSpeed = 20.0f;
	CMC->BrakingDecelerationWalking = 2000.0f;
	CMC->BrakingDecelerationFalling = 1500.0f;

	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->SetupAttachment(RootComponent);
	CameraBoom->TargetArmLength = 400.0f;
	CameraBoom->bUsePawnControlRotation = true;

	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
	FollowCamera->bUsePawnControlRotation = false;

	// AttributeComponent migrou pro PlayerState (AZMMOPlayerState + Component
	// Registry config-driven). Pawn fica leve — so' movement/input/camera.

	// Defaults visuais (mesh + AnimBP) — alinhados ao ZClientMMO. Permitem ao
	// motor spawnar AZMMOPlayerCharacter directamente como DefaultPawnClass do
	// AZMMOGameMode sem exigir um BP filho. Um BP filho continua opcional para
	// trocar mesh/AnimBP por mapa.
	if (USkeletalMeshComponent* MeshComponent = GetMesh())
	{
		static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
			TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple"));
		static ConstructorHelpers::FClassFinder<UAnimInstance> QuinnAnimBp(
			TEXT("/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed"));

		if (QuinnMesh.Succeeded())
		{
			MeshComponent->SetSkeletalMesh(QuinnMesh.Object);
			MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
			MeshComponent->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
		}
		else
		{
			UE_LOG(LogZMMOPlayer, Warning, TEXT("Default mesh SKM_Quinn_Simple not found for AZMMOPlayerCharacter."));
		}

		if (QuinnAnimBp.Succeeded())
		{
			MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
			MeshComponent->SetAnimInstanceClass(QuinnAnimBp.Class);
		}
		else
		{
			UE_LOG(LogZMMOPlayer, Warning, TEXT("Default anim blueprint ABP_Unarmed not found for AZMMOPlayerCharacter."));
		}
	}

	// Input Actions defaults (Enhanced Input) — IMCs sao adicionados pelo
	// AZMMOPlayerController. As IAs aqui resolvem o asset por path; um BP filho
	// pode sobrescrever caso queira inputs custom.
	static ConstructorHelpers::FObjectFinder<UInputAction> MoveActionAsset(
		TEXT("/Game/Input/Actions/IA_Move.IA_Move"));
	static ConstructorHelpers::FObjectFinder<UInputAction> LookActionAsset(
		TEXT("/Game/Input/Actions/IA_Look.IA_Look"));
	static ConstructorHelpers::FObjectFinder<UInputAction> MouseLookActionAsset(
		TEXT("/Game/Input/Actions/IA_MouseLook.IA_MouseLook"));
	static ConstructorHelpers::FObjectFinder<UInputAction> JumpActionAsset(
		TEXT("/Game/Input/Actions/IA_Jump.IA_Jump"));

	if (MoveActionAsset.Succeeded()) { MoveAction = MoveActionAsset.Object; }
	if (LookActionAsset.Succeeded()) { LookAction = LookActionAsset.Object; }
	if (MouseLookActionAsset.Succeeded()) { MouseLookAction = MouseLookActionAsset.Object; }
	if (JumpActionAsset.Succeeded()) { JumpAction = JumpActionAsset.Object; }
}

void AZMMOPlayerCharacter::BeginPlay()
{
	Super::BeginPlay();
	ResolveZeusNetworkSubsystem();
	BindZeusSpawnDelegate();
	TryRegisterLocalEntityFromCachedSpawn();

	// Fase 4: reposiciona o pawn na pos salva no DB (vinda no S_CHAR_SELECT_OK
	// e memorizada no Flow). Substitui o PlayerStart default do level. Se nao
	// ha pose pendente (primeiro boot / debug PIE direto no level), mantem o
	// PlayerStart.
	if (const UGameInstance* GI = GetGameInstance())
	{
		if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
		{
			FVector PosCm = FVector::ZeroVector;
			float YawDeg = 0.0f;
			if (Flow->ConsumePendingSpawnPose(PosCm, YawDeg))
			{
				const FRotator NewRot(0.0f, YawDeg, 0.0f);
				SetActorLocationAndRotation(PosCm, NewRot, /*bSweep=*/false, nullptr, ETeleportType::TeleportPhysics);
				if (AController* C = GetController())
				{
					C->SetControlRotation(NewRot);
				}
				UE_LOG(LogZMMOPlayer, Log,
					TEXT("AZMMOPlayerCharacter: pawn reposicionado pra pos do DB (%s) yaw=%.1f"),
					*PosCm.ToString(), YawDeg);
			}
		}
	}
}

void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
	UnbindZeusSpawnDelegate();
	ZeusNetwork = nullptr;
	Super::EndPlay(EndPlayReason);
}

void AZMMOPlayerCharacter::Tick(const float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);
	FlushInputAxisToServer(DeltaSeconds);
}

void AZMMOPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
	{
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AZMMOPlayerCharacter::OnJumpPressed);
		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AZMMOPlayerCharacter::OnJumpReleased);

		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Move);
		EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AZMMOPlayerCharacter::MoveCompleted);

		EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Look);
		EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Look);
	}
	else
	{
		UE_LOG(LogZMMOPlayer, Error, TEXT("'%s' Failed to find an Enhanced Input component."), *GetNameSafe(this));
	}
}

void AZMMOPlayerCharacter::Move(const FInputActionValue& Value)
{
	const FVector2D MovementVector = Value.Get<FVector2D>();
	DoMove(MovementVector.X, MovementVector.Y);
}

void AZMMOPlayerCharacter::MoveCompleted(const FInputActionValue& Value)
{
	(void)Value;
	DoMove(0.0f, 0.0f);
}

void AZMMOPlayerCharacter::Look(const FInputActionValue& Value)
{
	const FVector2D LookAxisVector = Value.Get<FVector2D>();
	DoLook(LookAxisVector.X, LookAxisVector.Y);
}

void AZMMOPlayerCharacter::OnJumpPressed()
{
	bPendingJumpPressed = true;
	DoJumpStart();
}

void AZMMOPlayerCharacter::OnJumpReleased()
{
	bPendingJumpReleased = true;
	DoJumpEnd();
}

void AZMMOPlayerCharacter::DoMove(const float Right, const float Forward)
{
	PendingMoveRight = FMath::Clamp(Right, -1.0f, 1.0f);
	PendingMoveForward = FMath::Clamp(Forward, -1.0f, 1.0f);

	if (GetController() != nullptr)
	{
		const FRotator Rotation = GetController()->GetControlRotation();
		const FRotator YawRotation(0.0f, Rotation.Yaw, 0.0f);
		const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
		const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
		AddMovementInput(ForwardDirection, Forward);
		AddMovementInput(RightDirection, Right);
	}
}

void AZMMOPlayerCharacter::DoLook(const float Yaw, const float Pitch)
{
	if (GetController() != nullptr)
	{
		AddControllerYawInput(Yaw);
		AddControllerPitchInput(Pitch);
	}
}

void AZMMOPlayerCharacter::DoJumpStart()
{
	Jump();
}

void AZMMOPlayerCharacter::DoJumpEnd()
{
	StopJumping();
}

void AZMMOPlayerCharacter::ApplyEntitySnapshot(const FZMMOEntitySnapshot& /*Snapshot*/)
{
	// Cliente local solto: o servidor nao reconcilia posicao em V0 (ADR 0038).
	// Quando colisao de objetos chegar, podemos ligar uma reconciliacao opcional
	// horizontal aqui (XY only), preservando o Z do CMC local.
}

void AZMMOPlayerCharacter::SetEntityRelevant(bool /*bRelevant*/)
{
	// Nunca despawnamos o jogador local via AOI.
}

void AZMMOPlayerCharacter::ResolveZeusNetworkSubsystem()
{
	if (ZeusNetwork)
	{
		return;
	}

	UGameInstance* GI = GetGameInstance();
	if (!GI)
	{
		return;
	}

	ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
}

void AZMMOPlayerCharacter::BindZeusSpawnDelegate()
{
	if (bSpawnDelegateBound || !IsLocallyControlled())
	{
		return;
	}
	if (!ZeusNetwork)
	{
		return;
	}

	ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
	bSpawnDelegateBound = true;
}

void AZMMOPlayerCharacter::UnbindZeusSpawnDelegate()
{
	if (!bSpawnDelegateBound || !ZeusNetwork)
	{
		bSpawnDelegateBound = false;
		return;
	}
	ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
	bSpawnDelegateBound = false;
}

void AZMMOPlayerCharacter::TryRegisterLocalEntityFromCachedSpawn()
{
	if (!ZeusNetwork)
	{
		return;
	}

	int32 CachedEntityId = 0;
	FVector CachedPosCm = FVector::ZeroVector;
	float CachedYawDeg = 0.0f;
	int64 CachedServerTimeMs = 0;
	if (ZeusNetwork->TryGetLastLocalSpawn(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs))
	{
		HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs);
	}
}

void AZMMOPlayerCharacter::HandleZeusPlayerSpawned(const int32 InEntityId, const bool bIsLocal,
	const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{
	if (!bIsLocal)
	{
		// Remote spawns sao tratados pelo `UZMMOWorldSubsystem`; aqui so reagimos
		// ao spawn do pawn possessivel local (S_SPAWN_PLAYER com bIsLocal=true).
		return;
	}
	HandleLocalSpawnReady(InEntityId, PosCm, YawDeg, ServerTimeMs);
}

void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const FVector PosCm,
	const float YawDeg, const int64 ServerTimeMs)
{
	if (InEntityId == 0)
	{
		return;
	}

	const int64 EntityIdAsInt64 = static_cast<int64>(InEntityId);
	if (ZMMOEntityId == EntityIdAsInt64)
	{
		return;
	}

	ZMMOEntityId = EntityIdAsInt64;

	UE_LOG(LogZMMOPlayer, Log,
		TEXT("AZMMOPlayerCharacter: local spawn captured EntityId=%d pos=(%s) yaw=%.1f t=%lld"),
		InEntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);

	if (UWorld* World = GetWorld())
	{
		if (UZMMOWorldSubsystem* WorldSubsystem = World->GetSubsystem<UZMMOWorldSubsystem>())
		{
			WorldSubsystem->RegisterLocalEntity(EntityIdAsInt64, this);
		}
	}

	// Seed do EntityId no AttributeComponent do PlayerState. O componente
	// vive no AZMMOPlayerState via Component Registry — busca via PlayerState
	// (sobrevive ao despawn do Pawn). S_ATTRIBUTE_SNAPSHOT_FULL chega via
	// UDP e e' rouado pelo ZMMOAttributeNetworkHandler via lookup por
	// EntityId no PlayerArray do GameState.
	if (APlayerState* PS = GetPlayerState())
	{
		if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass<UZMMOAttributeComponent>())
		{
			AttrComp->SeedEntityId(InEntityId);
		}
	}

	// UI in-game e' responsabilidade do AZMMOHUD (GameMode.HUDClass), nao do
	// Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que
	// pushea WBP_HUD em UI.Layer.Game. UZMMOHudWidget::NativeOnActivated auto-binda
	// no UZMMOAttributeComponent via PlayerState.
}

void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
{
	ResolveZeusNetworkSubsystem();
	if (!ZeusNetwork || !ZeusNetwork->IsConnected())
	{
		return;
	}

	const float SendInterval = 1.0f / FMath::Max(1, InputSendRateHz);
	SendAccumulatorSec += DeltaSeconds;
	TimeSinceLastSendSec += DeltaSeconds;

	const bool bMovingInput = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
	const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased;
	const bool bRateReached = SendAccumulatorSec >= SendInterval;
	const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;

	// IsFalling é estado contínuo do CMC (espelhado em PlayerStatePayload::grounded
	// para alimentar o MovementMode dos proxies). Forçar envio em transições
	// walking↔falling garante que a animação de queda/pulo dispare nos outros
	// clientes mesmo quando estamos em rate limit / sem input de movimento.
	const UCharacterMovementComponent* CMCForFalling = GetCharacterMovement();
	const bool bIsFalling = CMCForFalling != nullptr && CMCForFalling->IsFalling();
	const bool bFallingChanged = (bIsFalling != bPreviousFalling);

	const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
	if (!bShouldSend)
	{
		return;
	}

	++InputSequence;
	const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0);
	// ADR 0040: yaw da camara/controller para que o servidor consiga rotar o
	// input (referencial-camara) para velocidade mundial. Fallback para o yaw
	// do actor se nao houver controller (e.g. tela de loading).
	const float ViewYawDeg = (GetController()
		? static_cast<float>(GetController()->GetControlRotation().Yaw)
		: static_cast<float>(GetActorRotation().Yaw));
	// ADR 0041: cliente autoritativo da posicao XYZ + velocidade XY. O CMC ja
	// integrou o input local antes deste flush, portanto `GetActorLocation` e
	// `GetVelocity` reflectem o estado real que o servidor deve replicar para
	// os outros clientes (sujeito ao clamp anti-cheat).
	const FVector PosCm = GetActorLocation();
	const FVector Vel = GetVelocity();
	ZeusNetwork->SendInputAxis(
		PendingMoveForward,
		PendingMoveRight,
		bPendingJumpPressed,
		bPendingJumpReleased,
		InputSequence,
		ClientTimeMs,
		ViewYawDeg,
		PosCm,
		FVector2D(Vel.X, Vel.Y),
		bIsFalling,
		static_cast<float>(Vel.Z));

	SendAccumulatorSec = 0.0f;
	TimeSinceLastSendSec = 0.0f;
	bPendingJumpPressed = false;
	bPendingJumpReleased = false;
	bPreviousFalling = bIsFalling;
}
