// Assets/_Project/Scripts/Gameplay/BuildJob.cs
using System;
using Unity.Netcode;
using UnityEngine;

namespace TD.Gameplay
{
    /// <summary>
    /// One entry in a <see cref="Builder"/>'s build queue. Replicated as part of a
    /// <see cref="NetworkList{T}"/> on the Builder so all clients can render queued
    /// ghosts and progress without per-job RPCs.
    /// </summary>
    /// <remarks>
    /// <para><b>Identity.</b> <see cref="JobId"/> is a server-assigned, monotonically
    /// increasing identifier so cancel/lookup operations can target a specific job
    /// regardless of its current index in the NetworkList. Index-based addressing
    /// breaks the moment any job is removed.</para>
    ///
    /// <para><b>Stage transitions.</b> Jobs progress
    /// <see cref="BuildStage.Queued"/> → <see cref="BuildStage.Constructing"/> →
    /// (removed when complete). Only the head of the queue can transition to
    /// Constructing; tail jobs stay Queued until they reach the head.</para>
    ///
    /// <para><b>Time fields.</b> <see cref="ConstructionStartServerTime"/> is set on
    /// the server using <c>NetworkManager.ServerTime.TimeAsFloat</c> at the moment
    /// construction begins. Clients compute current stage as
    /// <c>floor(elapsed / (BuildTime / 4))</c> against the same server time, so all
    /// peers see identical staging without per-stage RPC chatter.</para>
    ///
    /// <para><b>INetworkSerializable.</b> Required for use in
    /// <see cref="NetworkList{T}"/>. Serializes only the minimum needed fields;
    /// derived data (footprint size, gold cost) is looked up from the
    /// TowerDefinition by <see cref="TowerTypeId"/> on each peer.</para>
    /// </remarks>
    [Serializable]
    public struct BuildJob : INetworkSerializable, IEquatable<BuildJob>
    {
        // ----- Persistent fields ------------------------------------------

        /// <summary>Server-assigned unique ID. Stable across NetworkList reorderings.</summary>
        public ulong JobId;

        /// <summary>Footprint anchor (SW corner, world-tile coords).</summary>
        public Vector2Int Anchor;

        /// <summary>Index into <c>TowerPlacementManager.towerDefinitions[]</c>.</summary>
        public int TowerTypeId;

        /// <summary>Current stage. See <see cref="BuildStage"/>.</summary>
        public BuildStage Stage;

        /// <summary>
        /// Server time (seconds) at which the current Constructing run began.
        /// -1 while the job is Queued or Paused (no active timer running).
        /// Read by clients to compute the current visual stage locally as
        /// <c>(now - ConstructionStartServerTime) + AccumulatedConstructionTime</c>.
        /// </summary>
        public float ConstructionStartServerTime;

        /// <summary>
        /// Construction time accumulated across previous Constructing runs.
        /// Used to preserve progress across pause/resume cycles.
        /// At pause, set to <c>elapsed_in_current_run + previous_accumulated</c>.
        /// At resume, <c>ConstructionStartServerTime</c> is reset to "now" and
        /// total progress is computed as
        /// <c>(now - ConstructionStartServerTime) + AccumulatedConstructionTime</c>.
        /// 0 for jobs that have never been paused.
        /// </summary>
        public float AccumulatedConstructionTime;

        /// <summary>
        /// Gold the player paid when this job was queued. Used to refund on
        /// cancellation. Stored on the job (not looked up from the definition)
        /// so that a future "tower price change mid-match" mechanic refunds
        /// what was actually paid.
        /// </summary>
        public int GoldSpent;

        // ----- Convenience constructors -----------------------------------

        public static BuildJob CreateQueued(ulong jobId, Vector2Int anchor, int towerTypeId, int goldSpent)
        {
            return new BuildJob
            {
                JobId = jobId,
                Anchor = anchor,
                TowerTypeId = towerTypeId,
                Stage = BuildStage.Queued,
                ConstructionStartServerTime = -1f,
                AccumulatedConstructionTime = 0f,
                GoldSpent = goldSpent,
            };
        }

        // ----- INetworkSerializable ---------------------------------------

        public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref JobId);
            serializer.SerializeValue(ref Anchor);
            serializer.SerializeValue(ref TowerTypeId);

            // BuildStage is a byte-backed enum; serialize as byte for forward-compat
            // and to avoid relying on default enum serialization assumptions.
            byte stageByte = (byte)Stage;
            serializer.SerializeValue(ref stageByte);
            Stage = (BuildStage)stageByte;

            serializer.SerializeValue(ref ConstructionStartServerTime);
            serializer.SerializeValue(ref AccumulatedConstructionTime);
            serializer.SerializeValue(ref GoldSpent);
        }

        // ----- IEquatable -------------------------------------------------
        //
        // NetworkList<T> requires IEquatable<T> in NGO 2.x. Critically, NGO's
        // indexer setter (jobs[i] = newValue) short-circuits the write when
        // Equals returns true — see NGO 2.7.0 changelog. If we only compared
        // by JobId, mutating Stage or ConstructionStartServerTime on a job and
        // writing it back would be silently dropped, leaving the list with the
        // old struct values forever. Compare every field that we ever mutate
        // after creation.
        //
        // GetHashCode uses JobId only because that's the unique identity and is
        // sufficient for hashing — the full-field comparison only happens on
        // hash collision (or where IEquatable bypasses the hash entirely,
        // which is the case in NetworkList's indexer setter).

        public bool Equals(BuildJob other)
        {
            return JobId == other.JobId
                && Anchor == other.Anchor
                && TowerTypeId == other.TowerTypeId
                && Stage == other.Stage
                && ConstructionStartServerTime == other.ConstructionStartServerTime
                && AccumulatedConstructionTime == other.AccumulatedConstructionTime
                && GoldSpent == other.GoldSpent;
        }

        public override bool Equals(object obj) => obj is BuildJob other && Equals(other);

        public override int GetHashCode() => JobId.GetHashCode();
    }

    /// <summary>
    /// Lifecycle stage of a <see cref="BuildJob"/>.
    /// </summary>
    /// <remarks>
    /// Backed by byte to keep the serialized payload small and to allow the byte
    /// round-trip in <see cref="BuildJob.NetworkSerialize{T}"/>.
    /// </remarks>
    public enum BuildStage : byte
    {
        /// <summary>
        /// Job is in the queue but the builder has not yet arrived at it.
        /// Tile is occupied (no other tower can be queued/placed there) but
        /// remains walkable — enemies pass through queued ghosts because the
        /// ghost represents intent, not a structure.
        /// </summary>
        Queued = 0,

        /// <summary>
        /// Builder has arrived and construction is in progress. Tile is
        /// occupied AND non-walkable — this is the moment the maze actually
        /// changes. Path re-validation runs at the transition into this stage.
        /// </summary>
        Constructing = 1,

        /// <summary>
        /// Construction was interrupted by the builder being moved away.
        /// Tile remains occupied and non-walkable (the half-built tower is
        /// still physical, still blocks enemies). The cube's Y-scale is frozen
        /// at the level it reached when paused. Resume returns to Constructing
        /// stage with previously-accumulated progress preserved.
        ///
        /// While the head job is Paused, the queue contains exactly this one
        /// job — all other queued jobs are refunded and removed at the moment
        /// of pausing.
        /// </summary>
        Paused = 2,
    }
}