using System.Text.Json;
using PlotLine.Models;
using PlotLine.Models.StoryIntelligence;

namespace PlotLine.Services;

public interface IStorySceneValidator
{
    ValidationResult Validate(SceneIntelligenceScene? scene);
}

public sealed class StorySceneValidator : IStorySceneValidator
{
    private static readonly HashSet<string> LocationTypes = CreateSet("named", "generic", "unclear");
    private static readonly HashSet<string> CharacterRoles = CreateSet("present", "pointOfView", "speaker", "mentioned", "unclear");
    private static readonly HashSet<string> KnowledgeChangeTypes = CreateSet("Learns", "Realises", "IsTold", "Discovers", "Confirms", "Suspects", "Misunderstands");
    private static readonly HashSet<string> ObservationTypes = CreateSet("CharacterAction", "CharacterKnowledge", "CharacterLocation", "RelationshipSignal", "AssetInteraction", "LocationSignal", "TimelineSignal", "QuestionSignal");
    private static readonly HashSet<string> EntityTypes = CreateSet("Character", "Location", "Asset", "Knowledge", "Relationship", "Timeline", "Scene", "Unknown");
    private static readonly HashSet<string> PreferredMetrics = CreateSet("action", "emotion", "tension", "conflict", "mystery", "romance", "humour", "revelation", "pacingIntensity");
    private static readonly HashSet<string> RelationshipSignals = CreateSet("trust", "mistrust", "affection", "fear", "authority", "secrecy", "conflict", "dependence", "protection", "rivalry", "obligation");

    public ValidationResult Validate(SceneIntelligenceScene? scene)
    {
        var result = new ValidationResult();

        try
        {
            if (scene is null)
            {
                AddError(result, "$", "Scene Intelligence JSON could not be deserialised into the V1 model.", "Return one valid Scene Intelligence JSON object.");
                return result;
            }

            ValidateRoot(scene, result);
            ValidateSceneReference(scene.SceneReference, result);
            ValidateSummary(scene.Summary, result);
            ValidateScenePurpose(scene.ScenePurpose, result);
            ValidatePointOfView(scene.PointOfView, result);
            ValidateSetting(scene.Setting, result);
            ValidateCharacters(scene.Characters, result);
            ValidateLocations(scene.Locations, result);
            ValidateAssets(scene.Assets, result);
            ValidateRelationships(scene.Relationships, result);
            ValidateKnowledgeChanges(scene.KnowledgeChanges, result);
            ValidateTimelineClues(scene.TimelineClues, result);
            ValidateQuestionsRaised(scene.QuestionsRaised, result);
            ValidateQuestionsAnswered(scene.QuestionsAnswered, result);
            ValidateObservations(scene.Observations, result);
            ValidateMetrics(scene.Metrics, result);
            ValidateSourceLimits(scene.SourceLimits, result);

            if (result.IsValid)
            {
                AddInfo(result, "$", "Validation passed.", "No action needed.");
            }
        }
        catch (Exception ex)
        {
            AddError(result, "$", $"Validator failed internally: {ex.Message}", "Review validator implementation for this response shape.");
        }

        return result;
    }

    private static void ValidateRoot(SceneIntelligenceScene scene, ValidationResult result)
    {
        RequiredString(scene.SchemaVersion, "schemaVersion", result);
        if (!string.Equals(scene.SchemaVersion, "1.0", StringComparison.Ordinal))
        {
            AddError(result, "schemaVersion", "Schema version must be \"1.0\".", "Return schemaVersion as \"1.0\".");
        }

        RequiredObject(scene.SceneReference, "sceneReference", result);
        RequiredObject(scene.Summary, "summary", result);
        RequiredObject(scene.ScenePurpose, "scenePurpose", result);
        RequiredObject(scene.PointOfView, "pointOfView", result);
        RequiredObject(scene.Setting, "setting", result);
        RequiredArray(scene.Characters, "characters", result);
        RequiredArray(scene.Locations, "locations", result);
        RequiredArray(scene.Assets, "assets", result);
        RequiredArray(scene.Relationships, "relationships", result);
        RequiredArray(scene.KnowledgeChanges, "knowledgeChanges", result);
        RequiredArray(scene.TimelineClues, "timelineClues", result);
        RequiredArray(scene.QuestionsRaised, "questionsRaised", result);
        RequiredArray(scene.QuestionsAnswered, "questionsAnswered", result);
        RequiredArray(scene.Observations, "observations", result);
        RequiredObject(scene.Metrics, "metrics", result);
        RequiredObject(scene.SourceLimits, "sourceLimits", result);
        Unknown(scene.ExtensionData, "$", result);
    }

    private static void ValidateSceneReference(SceneIntelligenceSceneReference? value, ValidationResult result)
    {
        if (value is null)
        {
            return;
        }

        RequiredScalar(value.ProjectId, "sceneReference.projectId", result, allowNull: true);
        RequiredScalar(value.BookId, "sceneReference.bookId", result, allowNull: true);
        RequiredScalar(value.ChapterId, "sceneReference.chapterId", result, allowNull: true);
        RequiredScalar(value.SceneId, "sceneReference.sceneId", result, allowNull: true);
        RequiredScalar(value.ChapterNumber, "sceneReference.chapterNumber", result, allowNull: true);
        RequiredScalar(value.SceneNumber, "sceneReference.sceneNumber", result, allowNull: true);
        RequiredString(value.SourceLabel, "sceneReference.sourceLabel", result);
        Unknown(value.ExtensionData, "sceneReference", result);
    }

    private static void ValidateSummary(SceneIntelligenceSummary? value, ValidationResult result)
    {
        if (value is null)
        {
            return;
        }

        RequiredString(value.Short, "summary.short", result);
        RequiredString(value.Detailed, "summary.detailed", result);
        Confidence(value.Confidence, "summary.confidence", result);
        Unknown(value.ExtensionData, "summary", result);
    }

    private static void ValidateScenePurpose(SceneIntelligenceScenePurpose? value, ValidationResult result)
    {
        if (value is null)
        {
            return;
        }

        RequiredString(value.ObservedFunction, "scenePurpose.observedFunction", result);
        Confidence(value.Confidence, "scenePurpose.confidence", result);
        Unknown(value.ExtensionData, "scenePurpose", result);
    }

    private static void ValidatePointOfView(SceneIntelligencePointOfView? value, ValidationResult result)
    {
        if (value is null)
        {
            return;
        }

        RequiredScalar(value.CharacterName, "pointOfView.characterName", result, allowNull: true);
        RequiredScalar(value.NarrativeMode, "pointOfView.narrativeMode", result, allowNull: true);
        Confidence(value.Confidence, "pointOfView.confidence", result);
        RequiredScalar(value.Evidence, "pointOfView.evidence", result, allowNull: true);
        Unknown(value.ExtensionData, "pointOfView", result);
    }

    private static void ValidateSetting(SceneIntelligenceSetting? value, ValidationResult result)
    {
        if (value is null)
        {
            return;
        }

        RequiredScalar(value.TimeOfDay, "setting.timeOfDay", result, allowNull: true);
        RequiredScalar(value.DateOrTimeReference, "setting.dateOrTimeReference", result, allowNull: true);
        RequiredScalar(value.LocationName, "setting.locationName", result, allowNull: true);
        Enum(value.LocationType, "setting.locationType", LocationTypes, result);
        RequiredScalar(value.GenericRoomType, "setting.genericRoomType", result, allowNull: true);
        RequiredScalar(value.ParentLocationHint, "setting.parentLocationHint", result, allowNull: true);
        Confidence(value.Confidence, "setting.confidence", result);

        if (string.Equals(value.LocationType, "generic", StringComparison.Ordinal) && string.IsNullOrWhiteSpace(value.GenericRoomType))
        {
            AddWarning(result, "setting.genericRoomType", "Generic setting has no generic room type.", "Include genericRoomType when the manuscript supports it.");
        }

        Unknown(value.ExtensionData, "setting", result);
    }

    private static void ValidateCharacters(List<SceneIntelligenceCharacter>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        DuplicateNames(values.Select((item, index) => (item.Name, Path: $"characters[{index}].name")), "characters", result);
        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"characters[{i}]";
            RequiredString(item.Name, $"{path}.name", result);
            Enum(item.RoleInScene, $"{path}.roleInScene", CharacterRoles, result);
            RequiredScalar(item.MentionedOnly, $"{path}.mentionedOnly", result);
            RequiredArray(item.Aliases, $"{path}.aliases", result);
            RequiredArray(item.Actions, $"{path}.actions", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            RequiredScalar(item.Notes, $"{path}.notes", result, allowNull: true);
            if (item.MentionedOnly == true && !string.Equals(item.RoleInScene, "mentioned", StringComparison.Ordinal))
            {
                AddWarning(result, $"{path}.roleInScene", "Mentioned-only character should usually use roleInScene \"mentioned\".", "Use roleInScene \"mentioned\" for absent references.");
            }

            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateLocations(List<SceneIntelligenceLocation>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        DuplicateNames(values.Select((item, index) => (item.Name, Path: $"locations[{index}].name")), "locations", result);
        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"locations[{i}]";
            RequiredString(item.Name, $"{path}.name", result);
            Enum(item.LocationType, $"{path}.locationType", LocationTypes, result);
            RequiredScalar(item.GenericRoomType, $"{path}.genericRoomType", result, allowNull: true);
            RequiredScalar(item.ParentLocationHint, $"{path}.parentLocationHint", result, allowNull: true);
            RequiredScalar(item.PresentInScene, $"{path}.presentInScene", result);
            RequiredScalar(item.MentionedOnly, $"{path}.mentionedOnly", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            RequiredScalar(item.Notes, $"{path}.notes", result, allowNull: true);
            if (item.PresentInScene == false && item.MentionedOnly == false)
            {
                AddWarning(result, $"{path}.mentionedOnly", "Location is not present but is not marked mentioned-only.", "Set mentionedOnly to true when a location is only discussed.");
            }

            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateAssets(List<SceneIntelligenceAsset>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        DuplicateNames(values.Select((item, index) => (item.Name, Path: $"assets[{index}].name")), "assets", result);
        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"assets[{i}]";
            RequiredString(item.Name, $"{path}.name", result);
            RequiredScalar(item.AssetType, $"{path}.assetType", result, allowNull: true);
            RequiredScalar(item.Status, $"{path}.status", result, allowNull: true);
            RequiredScalar(item.OwnerOrHolder, $"{path}.ownerOrHolder", result, allowNull: true);
            RequiredScalar(item.MentionedOnly, $"{path}.mentionedOnly", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            RequiredScalar(item.Notes, $"{path}.notes", result, allowNull: true);
            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateRelationships(List<SceneIntelligenceRelationship>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"relationships[{i}]";
            RequiredString(item.CharacterA, $"{path}.characterA", result);
            RequiredString(item.CharacterB, $"{path}.characterB", result);
            RequiredString(item.RelationshipSignal, $"{path}.relationshipSignal", result);
            if (!string.IsNullOrWhiteSpace(item.RelationshipSignal) && !RelationshipSignals.Contains(item.RelationshipSignal))
            {
                AddWarning(result, $"{path}.relationshipSignal", "Relationship signal is outside the preferred V1 neutral terms.", "Use a supported neutral signal when one fits.");
            }

            RequiredString(item.Evidence, $"{path}.evidence", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            var key = string.Join("|", new[] { item.CharacterA, item.CharacterB, item.RelationshipSignal }.Select(value => value?.Trim().ToLowerInvariant()));
            if (!string.IsNullOrWhiteSpace(key) && !seen.Add(key))
            {
                AddWarning(result, path, "Duplicate relationship signal found.", "Merge duplicate scene-local relationship signals when appropriate.");
            }

            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateKnowledgeChanges(List<SceneIntelligenceKnowledgeChange>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"knowledgeChanges[{i}]";
            RequiredString(item.RecipientCharacter, $"{path}.recipientCharacter", result);
            RequiredString(item.KnowledgeItem, $"{path}.knowledgeItem", result);
            Enum(item.ChangeType, $"{path}.changeType", KnowledgeChangeTypes, result);
            RequiredScalar(item.SourceCharacter, $"{path}.sourceCharacter", result, allowNull: true);
            RequiredScalar(item.SourceType, $"{path}.sourceType", result, allowNull: true);
            RequiredString(item.Evidence, $"{path}.evidence", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateTimelineClues(List<SceneIntelligenceTimelineClue>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"timelineClues[{i}]";
            RequiredString(item.Clue, $"{path}.clue", result);
            RequiredScalar(item.RelativeOrder, $"{path}.relativeOrder", result, allowNull: true);
            RequiredScalar(item.AbsoluteDate, $"{path}.absoluteDate", result, allowNull: true);
            RequiredString(item.Evidence, $"{path}.evidence", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateQuestionsRaised(List<SceneIntelligenceQuestionRaised>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"questionsRaised[{i}]";
            RequiredString(item.Question, $"{path}.question", result);
            RequiredString(item.Scope, $"{path}.scope", result);
            RequiredString(item.Evidence, $"{path}.evidence", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateQuestionsAnswered(List<SceneIntelligenceQuestionAnswered>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"questionsAnswered[{i}]";
            RequiredString(item.Question, $"{path}.question", result);
            RequiredString(item.Answer, $"{path}.answer", result);
            RequiredString(item.Evidence, $"{path}.evidence", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateObservations(List<SceneIntelligenceObservation>? values, ValidationResult result)
    {
        if (values is null)
        {
            return;
        }

        for (var i = 0; i < values.Count; i++)
        {
            var item = values[i];
            var path = $"observations[{i}]";
            Enum(item.ObservationType, $"{path}.observationType", ObservationTypes, result);
            Enum(item.SubjectEntityType, $"{path}.subjectEntityType", EntityTypes, result);
            RequiredString(item.SubjectName, $"{path}.subjectName", result);
            NullableEnum(item.ObjectEntityType, $"{path}.objectEntityType", EntityTypes, result);
            RequiredScalar(item.ObjectName, $"{path}.objectName", result, allowNull: true);
            RequiredString(item.Predicate, $"{path}.predicate", result);
            RequiredString(item.Description, $"{path}.description", result);
            RequiredString(item.Evidence, $"{path}.evidence", result);
            Confidence(item.Confidence, $"{path}.confidence", result);
            if (string.IsNullOrWhiteSpace(item.ObjectEntityType) != string.IsNullOrWhiteSpace(item.ObjectName))
            {
                AddWarning(result, path, "Observation object entity type and object name are inconsistent.", "Use both objectEntityType and objectName, or set both to null when no object applies.");
            }

            Unknown(item.ExtensionData, path, result);
        }
    }

    private static void ValidateMetrics(Dictionary<string, SceneIntelligenceMetric>? metrics, ValidationResult result)
    {
        if (metrics is null)
        {
            return;
        }

        foreach (var (name, metric) in metrics)
        {
            var path = $"metrics.{name}";
            if (!PreferredMetrics.Contains(name))
            {
                AddWarning(result, path, "Metric is outside the preferred V1 metric set.", "Use preferred V1 metrics unless the schema is intentionally expanded.");
            }

            if (metric is null)
            {
                AddError(result, path, "Metric object is required when a metric is present.", "Return { \"score\": 1-10, \"confidence\": 0.0-1.0 }.");
                continue;
            }

            if (!metric.Score.HasValue)
            {
                AddError(result, $"{path}.score", "Metric score is required.", "Return a score from 1 to 10.");
            }
            else if (metric.Score < 1 || metric.Score > 10)
            {
                AddError(result, $"{path}.score", "Metric score must be from 1 to 10.", "Use the V1 1-10 descriptive intensity scale.");
            }

            Confidence(metric.Confidence, $"{path}.confidence", result);
            Unknown(metric.ExtensionData, path, result);
        }
    }

    private static void ValidateSourceLimits(SceneIntelligenceSourceLimits? value, ValidationResult result)
    {
        if (value is null)
        {
            return;
        }

        RequiredScalar(value.ContainsAmbiguity, "sourceLimits.containsAmbiguity", result);
        RequiredArray(value.AmbiguityNotes, "sourceLimits.ambiguityNotes", result);
        Unknown(value.ExtensionData, "sourceLimits", result);
    }

    private static void Confidence(decimal? value, string path, ValidationResult result)
    {
        if (!value.HasValue)
        {
            AddError(result, path, "Confidence is required.", "Return a confidence value from 0.0 to 1.0.");
            return;
        }

        if (value < 0 || value > 1)
        {
            AddError(result, path, "Confidence must be from 0.0 to 1.0.", "Use the V1 confidence scale.");
        }
    }

    private static void Enum(string? value, string path, HashSet<string> allowed, ValidationResult result)
    {
        RequiredString(value, path, result);
        if (!string.IsNullOrWhiteSpace(value) && !allowed.Contains(value))
        {
            AddError(result, path, $"Value \"{value}\" is not allowed.", $"Use one of: {string.Join(", ", allowed)}.");
        }
    }

    private static void NullableEnum(string? value, string path, HashSet<string> allowed, ValidationResult result)
    {
        if (!string.IsNullOrWhiteSpace(value) && !allowed.Contains(value))
        {
            AddError(result, path, $"Value \"{value}\" is not allowed.", $"Use null or one of: {string.Join(", ", allowed)}.");
        }
    }

    private static void RequiredString(string? value, string path, ValidationResult result)
    {
        if (value is null)
        {
            AddError(result, path, "Required string property is missing or null.", "Return a supported string value.");
        }
        else if (string.IsNullOrWhiteSpace(value))
        {
            AddError(result, path, "Required string property is empty.", "Return a non-empty string supported by the scene.");
        }
    }

    private static void RequiredScalar<T>(T? value, string path, ValidationResult result, bool allowNull = false)
    {
        if (value is null && !allowNull)
        {
            AddError(result, path, "Required property is missing or null.", "Return the required property with a supported value.");
        }
    }

    private static void RequiredObject<T>(T? value, string path, ValidationResult result)
    {
        if (value is null)
        {
            AddError(result, path, "Required object is missing or null.", "Return the required V1 object.");
        }
    }

    private static void RequiredArray<T>(List<T>? value, string path, ValidationResult result)
    {
        if (value is null)
        {
            AddError(result, path, "Required array is missing or null.", "Return an empty array when no items are supported.");
        }
    }

    private static void RequiredObject<TValue>(Dictionary<string, TValue>? value, string path, ValidationResult result)
    {
        if (value is null)
        {
            AddError(result, path, "Required object is missing or null.", "Return an empty object when no metrics are supported.");
        }
    }

    private static void DuplicateNames(IEnumerable<(string? Name, string Path)> values, string collectionPath, ValidationResult result)
    {
        var seen = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var (name, path) in values)
        {
            if (string.IsNullOrWhiteSpace(name))
            {
                continue;
            }

            var key = name.Trim();
            if (seen.TryGetValue(key, out var existingPath))
            {
                AddWarning(result, path, $"Duplicate entity name also appears at {existingPath}.", $"Avoid duplicate names in {collectionPath} unless they represent distinct supported entities.");
            }
            else
            {
                seen[key] = path;
            }
        }
    }

    private static void Unknown(Dictionary<string, JsonElement>? values, string path, ValidationResult result)
    {
        if (values is null || values.Count == 0)
        {
            return;
        }

        foreach (var propertyName in values.Keys.Order(StringComparer.Ordinal))
        {
            AddWarning(result, path == "$" ? propertyName : $"{path}.{propertyName}", "Unknown property is not part of Scene Intelligence Schema V1.", "Remove this property or update the schema version intentionally.");
        }
    }

    private static void AddError(ValidationResult result, string path, string message, string suggestedFix)
        => result.Errors.Add(new ValidationIssue { Severity = "Error", Path = path, Message = message, SuggestedFix = suggestedFix });

    private static void AddWarning(ValidationResult result, string path, string message, string suggestedFix)
        => result.Warnings.Add(new ValidationIssue { Severity = "Warning", Path = path, Message = message, SuggestedFix = suggestedFix });

    private static void AddInfo(ValidationResult result, string path, string message, string suggestedFix)
        => result.Information.Add(new ValidationIssue { Severity = "Information", Path = path, Message = message, SuggestedFix = suggestedFix });

    private static HashSet<string> CreateSet(params string[] values)
        => new(values, StringComparer.Ordinal);
}
