// Copyright ExByte Studios. Item Interaction Ecosystem.

#include "ItemInteractionComponent.h"
#include "InteractionResolver.h"
#include "InteractableComponent.h"
#include "InteractionAction.h"
#include "ItemInteractionInterfaces.h"
#include "ItemInteractionLog.h"
#include "ItemNativeTags.h"
#include "InventoryComponent.h"
#include "ItemTransaction.h"
#include "GameFramework/Pawn.h"
#include "Engine/World.h"
#include "Net/UnrealNetwork.h"

UItemInteractionComponent::UItemInteractionComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
	PrimaryComponentTick.bStartWithTickEnabled = false;
	SetIsReplicatedByDefault(true);
}

void UItemInteractionComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME_CONDITION(UItemInteractionComponent, ActiveAction, COND_OwnerOnly);
}

void UItemInteractionComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	if (!bHasPending || !GetOwner() || !GetOwner()->HasAuthority())
	{
		return;
	}

	// Interrupt if the player walked away from the target (section 19).
	if (PendingRequest.TargetActor && PendingMaxDistanceSq > 0.f)
	{
		const float DistSq = FVector::DistSquared(GetOwner()->GetActorLocation(), PendingRequest.TargetActor->GetActorLocation());
		if (DistSq > PendingMaxDistanceSq)
		{
			AbortTimedAction();
			return;
		}
	}

	if (GetWorld()->GetTimeSeconds() >= PendingEndTime)
	{
		CompleteTimedAction();
	}
}

APawn* UItemInteractionComponent::GetPawn() const
{
	return Cast<APawn>(GetOwner());
}

void UItemInteractionComponent::RequestInteraction(const FInteractionOption& Option)
{
	// Opening a container is a purely local UI action: the shared container already
	// replicates to this client, so no server round-trip is needed (section 23).
	if (Option.Mode == EItemInteractionMode::OpenLootUI)
	{
		OnOpenContainer.Broadcast(Option.Request.TargetActor);
		return;
	}

	FInteractionRequest Request = Option.Request;
	if (!Request.RequestId.IsValid())
	{
		Request.RequestId = FGuid::NewGuid();
	}
	SendRequest(Request);
}

void UItemInteractionComponent::SendRequest(const FInteractionRequest& Request)
{
	Server_RequestInteraction(Request);
}

void UItemInteractionComponent::Server_RequestInteraction_Implementation(const FInteractionRequest& Request)
{
	FInteractionContext Context;
	FInteractionOption Authorized;
	if (!AuthorizeRequest(Request, Context, Authorized))
	{
		Client_InteractionResult(Request.RequestId, false);
		return;
	}

	// Timed / hold actions run over time with interruption (section 19); instant ones execute now.
	const bool bTimed = Authorized.ActionDuration > 0.f || Authorized.bRequiresHold;
	if (bTimed && !bHasPending)
	{
		BeginTimedAction(Context, Authorized, Request);
		return;
	}

	const bool bSuccess = ExecuteAuthorized(Context, Authorized, Request);
	Client_InteractionResult(Request.RequestId, bSuccess);
}

bool UItemInteractionComponent::AuthorizeRequest(const FInteractionRequest& Request, FInteractionContext& OutContext, FInteractionOption& OutOption) const
{
	APawn* Pawn = GetPawn();
	AActor* Target = Request.TargetActor;
	if (!Pawn || !Target)
	{
		UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: missing pawn/target."), *Request.ActionTag.ToString());
		return false;
	}

	// Distance + line of sight gate (section 9.3-9.4).
	OutContext = UInteractionResolver::BuildContext(Pawn, Target);
	if (UInteractableComponent* Interactable = OutContext.TargetInteractable)
	{
		const float MaxDist = Interactable->InteractionDistance + ServerDistanceTolerance;
		if (FVector::DistSquared(Pawn->GetActorLocation(), Target->GetActorLocation()) > FMath::Square(MaxDist))
		{
			UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: out of range."), *Request.ActionTag.ToString());
			return false;
		}
		if (Interactable->bIsBusy)
		{
			UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: target busy."), *Request.ActionTag.ToString());
			return false;
		}
	}

	// Re-resolve and require the requested option to actually be available + enabled (section 9.8-9.9).
	TArray<FInteractionOption> Options;
	UInteractionResolver::GetAvailableInteractions(Pawn, Target, Options);
	for (const FInteractionOption& Opt : Options)
	{
		if (Opt.ActionTag == Request.ActionTag && Opt.Mode == Request.Mode)
		{
			if (!Opt.bIsEnabled)
			{
				UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: %s"),
					*Request.ActionTag.ToString(), *Opt.DisabledReason.ToString());
				return false;
			}
			OutOption = Opt;
			return true;
		}
	}

	UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: option not available."), *Request.ActionTag.ToString());
	return false;
}

bool UItemInteractionComponent::ServerExecuteMode(const FInteractionContext& Context, EItemInteractionMode Mode, FGameplayTag ActionTag)
{
	if (!GetOwner() || !GetOwner()->HasAuthority())
	{
		return false;
	}
	FInteractionOption Option;
	Option.Mode = Mode;
	Option.ActionTag = ActionTag;

	FInteractionRequest Request;
	Request.Mode = Mode;
	Request.ActionTag = ActionTag;
	Request.TargetActor = Context.TargetActor;
	if (Context.bHasHeldItem) { Request.ItemInstanceId = Context.HeldItem.InstanceId; }
	else if (Context.bHasTargetItem) { Request.ItemInstanceId = Context.TargetItem.InstanceId; }

	return ExecuteAuthorized(Context, Option, Request);
}

bool UItemInteractionComponent::ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
{
	switch (Option.Mode)
	{
	case EItemInteractionMode::InstantPickup:
	case EItemInteractionMode::PickupToInventory:
		return ExecutePickup(Context);

	case EItemInteractionMode::TransferContainerItem:
		return ExecuteTransfer(Context, Request);

	default:
		// Anything else routes to a matching action-class on the interactable.
		// Built-in carry/equip/install/placement modes are implemented in their
		// respective stages (held/equipment/world-slot/placement modules).
		if (ExecuteActionClass(Context, Option))
		{
			return true;
		}
		UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] Mode %d has no handler yet."), (int32)Option.Mode);
		return false;
	}
}

bool UItemInteractionComponent::ExecutePickup(const FInteractionContext& Context)
{
	if (!Context.TargetActor || !Context.PlayerInventory || !Context.TargetActor->Implements<UWorldItemProvider>())
	{
		return false;
	}
	IWorldItemProvider* Provider = Cast<IWorldItemProvider>(Context.TargetActor);
	if (!Provider || !Provider->IsItemAvailable())
	{
		return false;
	}

	FItemInstanceData Item;
	if (!Provider->GetProvidedItem(Item) || !Item.IsValid())
	{
		return false;
	}

	const int32 Before = Item.Quantity;
	Context.PlayerInventory->AddItem(Item); // Item.Quantity now holds the leftover
	const int32 Added = Before - Item.Quantity;
	if (Added <= 0)
	{
		return false;
	}

	UE_LOG(LogItemInteraction, Log, TEXT("[Interaction] %s picked up %s x%d"),
		*GetNameSafe(GetOwner()), *Item.ItemId.ToString(), Added);

	if (Item.Quantity <= 0)
	{
		Provider->NotifyItemTaken(GetOwner());
	}
	else
	{
		Provider->UpdateProvidedItem(Item);
	}
	return true;
}

bool UItemInteractionComponent::ExecuteActionClass(const FInteractionContext& Context, const FInteractionOption& Option)
{
	if (!Context.TargetInteractable)
	{
		return false;
	}
	for (UInteractionAction* Action : Context.TargetInteractable->AvailableActions)
	{
		if (Action && Action->ActionTag == Option.ActionTag)
		{
			FText Reason;
			if (!Action->CanExecute(Context, Reason))
			{
				UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] %s: %s"),
					*Option.ActionTag.ToString(), *Reason.ToString());
				return false;
			}
			Action->Execute(Context);
			return true;
		}
	}
	return false;
}

// --- Cross-inventory transfer (loot) ---

bool UItemInteractionComponent::ExecuteTransfer(const FInteractionContext& Context, const FInteractionRequest& Request)
{
	UInventoryComponent* PlayerInv = Context.PlayerInventory;
	UInventoryComponent* StorageInv = Context.TargetActor ? Context.TargetActor->FindComponentByClass<UInventoryComponent>() : nullptr;
	if (!PlayerInv)
	{
		return false;
	}

	// Determine which inventory currently holds the item -> source; the other -> dest.
	FItemInstanceData Item;
	UInventoryComponent* Source = nullptr;
	UInventoryComponent* Dest = nullptr;
	if (PlayerInv->FindItem(Request.ItemInstanceId, Item)) { Source = PlayerInv; Dest = StorageInv; }
	else if (StorageInv && StorageInv->FindItem(Request.ItemInstanceId, Item)) { Source = StorageInv; Dest = PlayerInv; }

	if (!Source || !Dest || !Item.IsValid() || Item.Lock.bLocked)
	{
		return false;
	}

	// Pick the destination container.
	FGuid DestContainer = Request.TargetContainerId;
	if (!DestContainer.IsValid() || !Dest->CanAcceptItem(Item, DestContainer))
	{
		TArray<FInventoryContainer> Containers;
		Dest->GetContainers(Containers);
		DestContainer = FGuid();
		for (const FInventoryContainer& C : Containers)
		{
			if (Dest->CanAcceptItem(Item, C.ContainerId)) { DestContainer = C.ContainerId; break; }
		}
	}
	if (!DestContainer.IsValid())
	{
		UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] Transfer failed: no space."));
		return false;
	}

	// Atomic remove-from-source + add-to-dest (rolls back if either fails).
	FItemTransaction Tx;
	Tx.Instigator = GetOwner();
	Tx.Remove(Source, Item.InstanceId).Add(Dest, Item);
	if (!Tx.Apply())
	{
		UE_LOG(LogItemInteraction, Warning, TEXT("[ServerReject] Transfer transaction failed."));
		return false;
	}
	UE_LOG(LogItemInteraction, Log, TEXT("[Inventory] transferred %s x%d"), *Item.ItemId.ToString(), Item.Quantity);
	return true;
}

// --- Inventory UI operation entry points ---

void UItemInteractionComponent::RequestMoveItem(AActor* StorageActor, FGuid InstanceId, FGuid ToContainerId, int32 ToSlot)
{
	Server_MoveItem(StorageActor, InstanceId, ToContainerId, ToSlot);
}

void UItemInteractionComponent::RequestSplitStack(FGuid InstanceId, int32 Amount)
{
	Server_SplitStack(InstanceId, Amount);
}

void UItemInteractionComponent::RequestMergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId)
{
	Server_MergeStack(SourceInstanceId, TargetInstanceId);
}

void UItemInteractionComponent::RequestDropFromInventory(FGuid InstanceId)
{
	Server_DropFromInventory(InstanceId);
}

void UItemInteractionComponent::Server_MoveItem_Implementation(AActor* StorageActor, FGuid InstanceId, FGuid ToContainerId, int32 ToSlot)
{
	APawn* Pawn = GetPawn();
	UInventoryComponent* PlayerInv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
	if (!PlayerInv)
	{
		return;
	}

	if (StorageActor)
	{
		// Cross-inventory transfer; reuse the validated transfer path.
		FInteractionContext Ctx = UInteractionResolver::BuildContext(Pawn, StorageActor);
		FInteractionRequest Req;
		Req.ItemInstanceId = InstanceId;
		Req.TargetContainerId = ToContainerId;
		ExecuteTransfer(Ctx, Req);
		return;
	}

	// Rearrange within the player's own inventory.
	FItemInstanceData Item;
	if (PlayerInv->FindItem(InstanceId, Item))
	{
		PlayerInv->MoveItem(InstanceId, Item.CurrentContainerId, ToContainerId.IsValid() ? ToContainerId : Item.CurrentContainerId, ToSlot);
	}
}

void UItemInteractionComponent::Server_SplitStack_Implementation(FGuid InstanceId, int32 Amount)
{
	APawn* Pawn = GetPawn();
	if (UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr)
	{
		Inv->SplitStack(InstanceId, Amount);
	}
}

void UItemInteractionComponent::Server_MergeStack_Implementation(FGuid SourceInstanceId, FGuid TargetInstanceId)
{
	APawn* Pawn = GetPawn();
	if (UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr)
	{
		Inv->MergeStack(SourceInstanceId, TargetInstanceId);
	}
}

void UItemInteractionComponent::Server_DropFromInventory_Implementation(FGuid InstanceId)
{
	HandleDropFromInventory(InstanceId);
}

bool UItemInteractionComponent::HandleDropFromInventory(FGuid InstanceId)
{
	// Base cannot spawn a world pickup (that lives in ItemWorld). Overridden there.
	UE_LOG(LogItemInteraction, Verbose, TEXT("HandleDropFromInventory: no world handler bound (use WorldInteractionComponent)."));
	return false;
}

// --- Timed / hold actions (section 19) ---

void UItemInteractionComponent::BeginTimedAction(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
{
	bHasPending = true;
	bPendingHold = Option.bRequiresHold;
	PendingContext = Context;
	PendingOption = Option;
	PendingRequest = Request;

	const float Duration = Option.ActionDuration > 0.f ? Option.ActionDuration : Option.HoldDuration;
	PendingEndTime = GetWorld()->GetTimeSeconds() + FMath::Max(0.05f, Duration);

	// Interrupt radius: the interactable reach + tolerance.
	float MaxDist = 99999.f;
	if (Context.TargetInteractable)
	{
		MaxDist = Context.TargetInteractable->InteractionDistance + ServerDistanceTolerance;
	}
	PendingMaxDistanceSq = FMath::Square(MaxDist);

	LockActionItem(true);

	ActiveAction.bActive = true;
	ActiveAction.ActionTag = Option.ActionTag;
	ActiveAction.Duration = Duration;
	OnActionStarted.Broadcast(Option.ActionTag, Duration); // server-side listeners

	SetComponentTickEnabled(true);
}

void UItemInteractionComponent::CompleteTimedAction()
{
	const FInteractionOption Option = PendingOption;
	const FInteractionContext Context = PendingContext;
	const FInteractionRequest Request = PendingRequest;

	LockActionItem(false);
	bHasPending = false;
	ActiveAction = FActiveInteractionState();
	SetComponentTickEnabled(false);

	const bool bSuccess = ExecuteAuthorized(Context, Option, Request);
	OnActionFinished.Broadcast(Option.ActionTag, bSuccess);
	Client_InteractionResult(Request.RequestId, bSuccess);
}

void UItemInteractionComponent::AbortTimedAction()
{
	if (!bHasPending)
	{
		return;
	}
	const FInteractionRequest Request = PendingRequest;
	const FGameplayTag Tag = PendingOption.ActionTag;

	LockActionItem(false);
	bHasPending = false;
	ActiveAction = FActiveInteractionState();
	SetComponentTickEnabled(false);

	OnActionFinished.Broadcast(Tag, false);
	Client_InteractionResult(Request.RequestId, false);
	UE_LOG(LogItemInteraction, Verbose, TEXT("[Interaction] action %s interrupted."), *Tag.ToString());
}

void UItemInteractionComponent::LockActionItem(bool bLock)
{
	APawn* Pawn = GetPawn();
	UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
	if (!Inv || !PendingRequest.ItemInstanceId.IsValid())
	{
		return;
	}
	FItemInstanceData Item;
	if (!Inv->FindItem(PendingRequest.ItemInstanceId, Item))
	{
		return; // item not in player inventory (e.g. held/world) - nothing to lock here
	}
	if (bLock)
	{
		Inv->SetItemLock(PendingRequest.ItemInstanceId, ItemEcosystemTags::Lock_Using, GetOwner(), 0.f);
	}
	else
	{
		Inv->ClearItemLock(PendingRequest.ItemInstanceId);
	}
}

void UItemInteractionComponent::ReleaseHold()
{
	Server_ReleaseHold();
}

void UItemInteractionComponent::Server_ReleaseHold_Implementation()
{
	if (bHasPending && bPendingHold)
	{
		AbortTimedAction(); // released before the hold completed
	}
}

void UItemInteractionComponent::CancelActiveAction()
{
	if (GetOwner() && GetOwner()->HasAuthority())
	{
		AbortTimedAction();
	}
}

void UItemInteractionComponent::OnRep_ActiveAction()
{
	if (ActiveAction.bActive)
	{
		OnActionStarted.Broadcast(ActiveAction.ActionTag, ActiveAction.Duration);
	}
	else
	{
		OnActionFinished.Broadcast(ActiveAction.ActionTag, true);
	}
}

void UItemInteractionComponent::Client_InteractionResult_Implementation(FGuid RequestId, bool bSuccess)
{
	OnInteractionResult.Broadcast(RequestId, bSuccess);
}
