using UnityEngine;

/// <summary>
/// Finds nearby enemies (tagged "Enemy") that satisfy the assist constraints.
/// Designed for zero-allocation queries during combat assists.
/// </summary>
[DisallowMultipleComponent]
public sealed class EnemyTargetDetector : MonoBehaviour
{
    private const float MinAssistRadius = 0.5f;

    [Header("Assist Window")]
    [Tooltip("Maximum distance at which the assist can consider enemies.")]
    [SerializeField] private float assistRadius = 4.0f;

    [Tooltip("Maximum field of view in degrees around the forward vector where assist is allowed.")]
    [SerializeField, Range(30f, 180f)] private float assistFOVDegrees = 130f;

    [Tooltip("Optional hard cap on the delta angle to avoid 180° turns.")]
    [SerializeField, Range(45f, 180f)] private float maxAssistAngle = 150f;

    [Tooltip("Layer mask used for enemy overlap queries.")]
    [SerializeField] private LayerMask enemyLayers = ~0;

    [Tooltip("Maximum number of enemy candidates that will be considered per query.")]
    [SerializeField, Min(1)] private int maxCandidates = 12;

    [Header("Line Of Sight")]
    [Tooltip("If enabled, a raycast must reach the target without hitting the obstruction layers.")]
    [SerializeField] private bool requireLineOfSight = false;

    [SerializeField] private LayerMask obstructionLayers = ~0;

    [SerializeField, Tooltip("Height offset for line of sight ray origin.")]
    private float losHeightOffset = 1.6f;

    [SerializeField, Tooltip("Height offset for line of sight ray destination.")]
    private float targetHeightOffset = 1.2f;

    private Collider[] candidateBuffer;
    private Transform hardLockOverride;

    private void Awake()
    {
        maxCandidates = Mathf.Max(1, maxCandidates);
        candidateBuffer = new Collider[maxCandidates];
        if (assistRadius < MinAssistRadius)
        {
            assistRadius = MinAssistRadius;
        }
    }

    /// <summary>
    /// Optionally sets a manually chosen target that should be prioritised over soft candidates.
    /// </summary>
    public void SetHardLockTarget(Transform target)
    {
        hardLockOverride = target;
    }

    /// <summary>
    /// Attempts to find the best enemy target within the configured assist window.
    /// </summary>
    public bool TryFindBestTarget(out Transform target, out Vector3 targetPosition)
    {
        target = null;
        targetPosition = Vector3.zero;

        if (!enabled)
            return false;

        if (hardLockOverride != null && IsTargetValid(hardLockOverride, out var hardLockPos))
        {
            target = hardLockOverride;
            targetPosition = hardLockPos;
            return true;
        }

        var origin = transform.position;
        var forward = transform.forward;
        forward.y = 0f;
        forward.Normalize();

        int hits = Physics.OverlapSphereNonAlloc(origin, assistRadius, candidateBuffer, enemyLayers, QueryTriggerInteraction.Collide);
        if (hits <= 0)
            return false;

        float bestScore = float.MaxValue;
        Transform bestTarget = null;
        Vector3 bestPosition = Vector3.zero;

        for (int i = 0; i < hits; i++)
        {
            Collider candidate = candidateBuffer[i];
            if (candidate == null)
                continue;

            Transform candidateTransform = candidate.transform;
            if (!candidateTransform.CompareTag("Enemy"))
                continue;

            if (!IsTargetValid(candidateTransform, out var candidatePosition))
                continue;

            Vector3 toTarget = candidatePosition - origin;
            toTarget.y = 0f;
            float sqrDist = toTarget.sqrMagnitude;
            if (sqrDist < 0.0001f)
                continue;

            Vector3 direction = toTarget / Mathf.Sqrt(sqrDist);
            float angle = Vector3.Angle(forward, direction);
            if (angle > assistFOVDegrees)
                continue;

            if (angle > maxAssistAngle)
                continue;

            if (requireLineOfSight && !HasLineOfSight(origin, candidatePosition))
                continue;

            // Score by angle first, then distance.
            float score = angle * 10f + sqrDist;
            if (score < bestScore)
            {
                bestScore = score;
                bestTarget = candidateTransform;
                bestPosition = candidatePosition;
            }
        }

        if (bestTarget == null)
            return false;

        target = bestTarget;
        targetPosition = bestPosition;
        return true;
    }

    private bool IsTargetValid(Transform candidate, out Vector3 targetPosition)
    {
        targetPosition = candidate.position;

        if (candidate == null)
            return false;

        // Verify active in hierarchy (common alive check surrogate).
        if (!candidate.gameObject.activeInHierarchy)
            return false;

        return true;
    }

    private bool HasLineOfSight(Vector3 origin, Vector3 targetPosition)
    {
        Vector3 rayOrigin = origin + Vector3.up * losHeightOffset;
        Vector3 rayTarget = targetPosition + Vector3.up * targetHeightOffset;
        Vector3 delta = rayTarget - rayOrigin;
        float distance = delta.magnitude;
        if (distance <= 0.001f)
            return true;

        Vector3 direction = delta / distance;
        if (Physics.Raycast(rayOrigin, direction, distance, obstructionLayers, QueryTriggerInteraction.Ignore))
        {
            return false;
        }

        return true;
    }

    #region Inspector API

    public float AssistRadius
    {
        get => assistRadius;
        set => assistRadius = Mathf.Max(MinAssistRadius, value);
    }

    public float AssistFOVDegrees
    {
        get => assistFOVDegrees;
        set => assistFOVDegrees = Mathf.Clamp(value, 30f, 180f);
    }

    public float MaxAssistAngle
    {
        get => maxAssistAngle;
        set => maxAssistAngle = Mathf.Clamp(value, 45f, 180f);
    }

    public LayerMask EnemyLayers
    {
        get => enemyLayers;
        set => enemyLayers = value;
    }

    public bool RequireLineOfSight
    {
        get => requireLineOfSight;
        set => requireLineOfSight = value;
    }

    public LayerMask ObstructionLayers
    {
        get => obstructionLayers;
        set => obstructionLayers = value;
    }

    #endregion
}
