#include "InteractionComponent.h"
#include "Interactable.h"
#include "Engine/OverlapResult.h"
#include "CollisionQueryParams.h"
#include "DrawDebugHelpers.h"
#include "NakedDesire/Player/NakedDesireCharacter.h"

UInteractionComponent::UInteractionComponent()
{
	PrimaryComponentTick.bCanEverTick = false;
}

void UInteractionComponent::BeginPlay()
{
	Super::BeginPlay();

	// Only run on the locally controlled pawn — outlines and focus are per-client visuals
	const APawn* OwnerPawn = Cast<APawn>(GetOwner());
	if (!OwnerPawn)
		return;

	GetWorld()->GetTimerManager().SetTimer(
		InteractionTimerHandle,
		this,
		&UInteractionComponent::UpdateInteraction,
		0.1f,
		true
	);
}

void UInteractionComponent::UpdateInteraction()
{
	UpdateNearbyInteractables();
	UpdateFocusedInteractable();
}

void UInteractionComponent::Interact()
{
	AActor* Target = FocusedInteractable.Get();
	if (!Target)
		return;

	if (!IInteractable::Execute_CanInteract(Target, Cast<ANakedDesireCharacter>(GetOwner())))
		return;
	IInteractable::Execute_Interact(Target, Cast<ANakedDesireCharacter>(GetOwner()));
}

void UInteractionComponent::UpdateNearbyInteractables()
{
	AActor* Owner = GetOwner();
	if (!Owner)
		return;

	// Sphere overlap to find all nearby actors
	TArray<FOverlapResult> Overlaps;
	FCollisionObjectQueryParams ObjectParams;
	ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
	ObjectParams.AddObjectTypesToQuery(ECC_WorldStatic);

	GetWorld()->OverlapMultiByObjectType(
		Overlaps,
		Owner->GetActorLocation(),
		FQuat::Identity,
		ObjectParams,
		FCollisionShape::MakeSphere(ProximityRadius)
	);

	// Build the new set from overlap results, filtering to IInteractable actors
	TSet<TWeakObjectPtr<AActor>> NewNearby;
	for (const FOverlapResult& Overlap : Overlaps)
	{
		AActor* Actor = Overlap.GetActor();
		if (Actor && Actor != Owner && Actor->Implements<UInteractable>())
		{
			NewNearby.Add(Actor);
		}
	}

	// Actors that left proximity: hide their hints
	for (const TWeakObjectPtr<AActor>& OldActor : NearbyInteractables)
	{
		if (OldActor.IsValid() && !NewNearby.Contains(OldActor))
		{
			if (FocusedInteractable == OldActor)
			{
				FocusedInteractable = nullptr;
				OnFocusedInteractableChanged.Broadcast(nullptr);
			}
			IInteractable::Execute_HideInteractionHint(OldActor.Get());
		}
	}

	NearbyInteractables = MoveTemp(NewNearby);

	// Recompute the proximity hint for every nearby actor based on current LOS.
	// Runs every tick so actors that enter/leave line-of-sight mid-session update
	// immediately — e.g. a wall sliding into place hides the hint without the
	// actor ever leaving the proximity radius.
	// The focused actor is skipped: UpdateFocusedInteractable owns its hint state.
	for (const TWeakObjectPtr<AActor>& WeakActor : NearbyInteractables)
	{
		AActor* Actor = WeakActor.Get();
		if (!Actor || FocusedInteractable == Actor)
			continue;

		if (HasLineOfSightFromPawn(Actor))
			IInteractable::Execute_ShowInteractionProximityHint(Actor);
		else
			IInteractable::Execute_HideInteractionHint(Actor);
	}
}

void UInteractionComponent::UpdateFocusedInteractable()
{
	FVector ViewLocation;
	FRotator ViewRotation;
	if (!GetOwnerViewPoint(ViewLocation, ViewRotation))
		return;

	const FVector ViewForward = ViewRotation.Vector();
	ANakedDesireCharacter* Player = Cast<ANakedDesireCharacter>(GetOwner());

	// Among nearby interactables, find the one most centered in the player's view
	// that is also within interaction distance and passes CanInteract
	AActor* BestActor = nullptr;
	float BestDot = -1.f;

	for (const TWeakObjectPtr<AActor>& WeakActor : NearbyInteractables)
	{
		AActor* Actor = WeakActor.Get();
		if (!Actor)
			continue;  

		if (FVector::DistSquared(Player->GetActorLocation(), Actor->GetActorLocation()) > FMath::Square(InteractionDistance))
			continue;

		if (!IInteractable::Execute_CanInteract(Actor, Player))
			continue;

		// Look-cone check — cheap filter before the LOS trace
		const FVector ToActor = (Actor->GetActorLocation() - ViewLocation).GetSafeNormal();
		const float Dot = FVector::DotProduct(ViewForward, ToActor);
		if (Dot < LookDotThreshold || Dot <= BestDot)
			continue;

		// LOS from the pawn's eye position — not the camera, so looking over a
		// wall with the third-person camera does not grant interaction range
		if (!HasLineOfSightFromPawn(Actor))
			continue;

		BestDot = Dot;
		BestActor = Actor;
	}

	if (FocusedInteractable.Get() == BestActor)
		return;

	// Restore the previously focused actor — only show a proximity hint if it is
	// still nearby AND in line-of-sight; hide it otherwise
	if (AActor* OldFocused = FocusedInteractable.Get())
	{
		if (NearbyInteractables.Contains(OldFocused) && HasLineOfSightFromPawn(OldFocused))
			IInteractable::Execute_ShowInteractionProximityHint(OldFocused);
		else
			IInteractable::Execute_HideInteractionHint(OldFocused);
	}

	FocusedInteractable = BestActor;

	if (BestActor)
		IInteractable::Execute_ShowInteractionFocusHint(BestActor);

	OnFocusedInteractableChanged.Broadcast(BestActor);
}

bool UInteractionComponent::GetOwnerViewPoint(FVector& OutLocation, FRotator& OutRotation) const
{
	const APawn* OwnerPawn = Cast<APawn>(GetOwner());
	if (!OwnerPawn)
		return false;

	AController* Controller = OwnerPawn->GetController();
	if (!Controller)
		return false;

	Controller->GetPlayerViewPoint(OutLocation, OutRotation);
	return true;
}

bool UInteractionComponent::HasLineOfSightFromPawn(AActor* Target) const
{
	const APawn* OwnerPawn = Cast<APawn>(GetOwner());
	if (!OwnerPawn || !Target)
		return false;

	// Start from the pawn's own eye position, NOT the spring-arm camera.
	// This means a player who tilts the camera above a wall still cannot
	// reach objects on the other side — the trace originates from the character mesh.
	const FVector Start = OwnerPawn->GetPawnViewLocation();
	const FVector End = Target->GetActorLocation();

	FCollisionQueryParams Params(SCENE_QUERY_STAT(InteractionLOS), false);
	Params.AddIgnoredActor(GetOwner());
	Params.AddIgnoredActor(Target);

	FHitResult Hit;
	const bool bBlocked = GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params);

#if ENABLE_DRAW_DEBUG
	if (bDrawDebugLineOfSight)
	{
		const FColor LineColor = bBlocked ? FColor::Red : FColor::Green;
		DrawDebugLine(GetWorld(), Start, bBlocked ? Hit.ImpactPoint : End, LineColor, false, 0.1f, 0, 1.f);
		if (bBlocked)
			DrawDebugPoint(GetWorld(), Hit.ImpactPoint, 10.f, FColor::Red, false, 0.1f);
	}
#endif

	return !bBlocked;
}