namespace JobTrackerApi.Services
{
    public enum PipelineCategory
    {
        /// <summary>
        /// Pre-application: the user is tracking the opportunity but has not applied yet.
        /// Nothing in these stages has been submitted, so follow-up/ghosting rules and
        /// applied-volume analytics must never count them.
        /// </summary>
        Prospect,
        Active,
        Success,
        Closed,
    }

    /// <summary>
    /// How stages collapse on the board. Deliberately NOT the same axis as
    /// <see cref="PipelineCategory"/>: Category carries analytics semantics (Offer is Success, so
    /// StageAnalytics excludes it from "how long has this been stuck"), while Group is what the
    /// user sees (Offer is still something you are actively working, so it groups under Active).
    /// Collapsing the two would force one to lie.
    /// </summary>
    public enum PipelineGroup
    {
        NotApplied,
        Active,
        Closed,
    }

    public sealed record PipelineStage(string Key, int Order, PipelineCategory Category, PipelineGroup Group);

    /// <summary>
    /// Canonical job-application pipeline: the single source of truth for the ordered set of
    /// statuses, their grouping, and how free-text/legacy values normalize onto them.
    /// Status remains a free-text column so custom values are never destroyed; this only
    /// canonicalizes casing and known synonyms.
    /// </summary>
    public static class JobPipeline
    {
        /// <summary>
        /// Fallback for an empty status on the legacy create path, which historically meant
        /// "already applied". New pre-application flows should pass <see cref="SavedStatus"/>
        /// explicitly rather than relying on this.
        /// </summary>
        public const string DefaultStatus = "Applied";

        /// <summary>Entry stage for a job captured before the user has applied.</summary>
        public const string SavedStatus = "Saved";

        /// <summary>
        /// The detailed internal stages. The board groups these (see <see cref="PipelineGroup"/>)
        /// rather than showing ten columns.
        ///
        /// Waiting and Ghosted are retained deliberately. Ghosted is where the rules engine parks
        /// a job that was never answered — it is neither Rejected (nobody rejected you) nor
        /// Withdrawn (you did not withdraw), and removing it would leave auto-ghosting with no
        /// target stage. Waiting carries its own follow-up rule and reminder wording.
        /// </summary>
        public static readonly IReadOnlyList<PipelineStage> Stages = new List<PipelineStage>
        {
            new("Saved", 1, PipelineCategory.Prospect, PipelineGroup.NotApplied),
            new("Interested", 2, PipelineCategory.Prospect, PipelineGroup.NotApplied),
            new("Preparing", 3, PipelineCategory.Prospect, PipelineGroup.NotApplied),
            new("Applied", 4, PipelineCategory.Active, PipelineGroup.Active),
            new("Waiting", 5, PipelineCategory.Active, PipelineGroup.Active),
            new("Interview", 6, PipelineCategory.Active, PipelineGroup.Active),
            new("Offer", 7, PipelineCategory.Success, PipelineGroup.Active),
            new("Rejected", 8, PipelineCategory.Closed, PipelineGroup.Closed),
            new("Ghosted", 9, PipelineCategory.Closed, PipelineGroup.Closed),
            new("Withdrawn", 10, PipelineCategory.Closed, PipelineGroup.Closed),
        };

        /// <summary>Stage keys in a group, in pipeline order.</summary>
        public static IReadOnlyList<string> StagesInGroup(PipelineGroup group)
            => Stages.Where(s => s.Group == group).OrderBy(s => s.Order).Select(s => s.Key).ToList();

        /// <summary>True when the status is a pre-application stage (nothing submitted yet).</summary>
        public static bool IsProspect(string? status)
        {
            var normalized = Normalize(status);
            var stage = Stages.FirstOrDefault(s => string.Equals(s.Key, normalized, StringComparison.OrdinalIgnoreCase));
            // Unknown custom statuses are NOT treated as prospects: they predate this split and
            // have always been counted as applied. Assuming otherwise would silently drop them
            // out of existing users' analytics.
            return stage?.Category == PipelineCategory.Prospect;
        }

        private static readonly Dictionary<string, string> Canonical =
            Stages.ToDictionary(s => s.Key, s => s.Key, StringComparer.OrdinalIgnoreCase);

        // Legacy/synonym spellings that should collapse onto a canonical stage.
        private static readonly Dictionary<string, string> Aliases = new(StringComparer.OrdinalIgnoreCase)
        {
            ["bookmarked"] = "Saved",
            ["wishlist"] = "Saved",
            ["to apply"] = "Saved",
            ["shortlisted"] = "Interested",
            ["considering"] = "Interested",
            ["in preparation"] = "Preparing",
            ["preparing application"] = "Preparing",
            ["drafting"] = "Preparing",
            ["interviewing"] = "Interview",
            ["interviews"] = "Interview",
            ["interviewed"] = "Interview",
            ["in interview"] = "Interview",
            ["awaiting response"] = "Waiting",
            ["awaiting"] = "Waiting",
            ["in progress"] = "Waiting",
            ["pending"] = "Waiting",
            ["no response"] = "Ghosted",
            ["no reply"] = "Ghosted",
            ["declined"] = "Rejected",
            // "declined" stays mapped to Rejected above (the employer declined you). Withdrawn is
            // the opposite direction — the user pulled out — so it takes only unambiguous spellings.
            ["withdrew"] = "Withdrawn",
            ["cancelled"] = "Withdrawn",
            ["canceled"] = "Withdrawn",
        };

        /// <summary>
        /// Returns the canonical status for a raw value: trims, matches a stage case-insensitively,
        /// or maps a known synonym. Unknown non-empty values are preserved (trimmed) so custom
        /// statuses survive. Empty/whitespace becomes the default stage.
        /// </summary>
        public static string Normalize(string? status)
        {
            var trimmed = (status ?? string.Empty).Trim();
            if (trimmed.Length == 0) return DefaultStatus;
            if (Canonical.TryGetValue(trimmed, out var canonical)) return canonical;
            if (Aliases.TryGetValue(trimmed, out var alias)) return alias;
            return trimmed;
        }

        public static bool IsCanonical(string? status)
            => !string.IsNullOrWhiteSpace(status) && Canonical.ContainsKey(status.Trim());

        /// <summary>
        /// Enforces the invariant "DateApplied is set if and only if the job has left the
        /// pre-application stages". Call after any write to <see cref="JobApplication.Status"/>;
        /// every status-write path routes through here so they cannot drift apart.
        ///
        /// Moving backwards into a Prospect stage clears DateApplied. That is deliberate: the
        /// alternative — a Saved job still carrying an applied date — silently counts it as
        /// applied in analytics and exposes it to the follow-up/ghosting rules.
        ///
        /// Returns the date that was cleared, or null if nothing was cleared. Callers persist it
        /// as an <see cref="Models.JobEvent"/> (Type = <see cref="AppliedDateClearedEvent"/>) so
        /// the application activity survives the clear — this method has no DbContext, so it
        /// reports what it did rather than recording it.
        /// </summary>
        public static DateTime? SyncAppliedDate(Models.JobApplication job, DateTime nowUtc)
        {
            if (IsProspect(job.Status))
            {
                var cleared = job.DateApplied;
                job.DateApplied = null;
                return cleared;
            }

            job.DateApplied ??= nowUtc;
            return null;
        }

        /// <summary>
        /// JobEvent.Type for a DateApplied cleared by a backwards move into a Prospect stage.
        /// OldValue holds the round-tripped date so the history is machine-readable, not just prose.
        /// </summary>
        public const string AppliedDateClearedEvent = "AppliedDateCleared";

        public static int OrderOf(string? status)
        {
            var normalized = Normalize(status);
            var stage = Stages.FirstOrDefault(s => string.Equals(s.Key, normalized, StringComparison.OrdinalIgnoreCase));
            return stage?.Order ?? int.MaxValue; // custom statuses sort last
        }
    }
}
