using System.Text.Json;
using ScadaLink.Commons.Types.Flattening;

namespace ScadaLink.TemplateEngine.Validation;

/// <summary>
/// Pre-deployment validation pipeline. Validates a FlattenedConfiguration for correctness
/// before deployment. Also available on-demand (same logic, no deployment trigger).
///
/// Validation checks:
/// 1. Flattening success (no empty configuration)
/// 2. No naming collisions
/// 3. Script compilation (via ScriptCompiler)
/// 4. Alarm trigger references exist (referenced attributes must be in the flattened config)
/// 5. Script trigger references exist (referenced attributes must be in the flattened config)
/// 6. Connection binding completeness (all data-sourced attributes must have a binding)
/// 7. Does NOT verify tag path resolution on devices
/// </summary>
public class ValidationService
{
    private readonly SemanticValidator _semanticValidator;
    private readonly ScriptCompiler _scriptCompiler;

    public ValidationService(SemanticValidator semanticValidator, ScriptCompiler scriptCompiler)
    {
        _semanticValidator = semanticValidator;
        _scriptCompiler = scriptCompiler;
    }

    /// <summary>
    /// Convenience constructor that creates default dependencies.
    /// </summary>
    public ValidationService() : this(new SemanticValidator(), new ScriptCompiler())
    {
    }

    /// <summary>
    /// Runs the full validation pipeline on a flattened configuration.
    /// </summary>
    public ValidationResult Validate(FlattenedConfiguration configuration, IReadOnlyList<ResolvedScript>? sharedScripts = null)
    {
        ArgumentNullException.ThrowIfNull(configuration);

        var results = new List<ValidationResult>
        {
            ValidateFlatteningSuccess(configuration),
            ValidateNamingCollisions(configuration),
            ValidateScriptCompilation(configuration),
            ValidateAlarmTriggerReferences(configuration),
            ValidateScriptTriggerReferences(configuration),
            ValidateConnectionBindingCompleteness(configuration),
            _semanticValidator.Validate(configuration, sharedScripts)
        };

        return ValidationResult.Merge(results.ToArray());
    }

    /// <summary>
    /// Validates that flattening produced a non-empty configuration.
    /// </summary>
    public static ValidationResult ValidateFlatteningSuccess(FlattenedConfiguration configuration)
    {
        var errors = new List<ValidationEntry>();

        if (string.IsNullOrWhiteSpace(configuration.InstanceUniqueName))
            errors.Add(ValidationEntry.Error(ValidationCategory.FlatteningFailure,
                "Instance unique name is missing."));

        if (configuration.Attributes.Count == 0 &&
            configuration.Alarms.Count == 0 &&
            configuration.Scripts.Count == 0)
        {
            return new ValidationResult
            {
                Warnings = [ValidationEntry.Warning(ValidationCategory.FlatteningFailure,
                    "Flattened configuration contains no attributes, alarms, or scripts.")]
            };
        }

        return errors.Count > 0
            ? new ValidationResult { Errors = errors }
            : ValidationResult.Success();
    }

    /// <summary>
    /// Validates that there are no naming collisions across entity types.
    /// Canonical names must be unique within their entity type (attributes, alarms, scripts).
    /// </summary>
    public static ValidationResult ValidateNamingCollisions(FlattenedConfiguration configuration)
    {
        var errors = new List<ValidationEntry>();

        CheckDuplicates(configuration.Attributes, a => a.CanonicalName, "Attribute", errors);
        CheckDuplicates(configuration.Alarms, a => a.CanonicalName, "Alarm", errors);
        CheckDuplicates(configuration.Scripts, s => s.CanonicalName, "Script", errors);

        return errors.Count > 0
            ? new ValidationResult { Errors = errors }
            : ValidationResult.Success();
    }

    /// <summary>
    /// Validates that all scripts compile successfully using the ScriptCompiler.
    /// </summary>
    public ValidationResult ValidateScriptCompilation(FlattenedConfiguration configuration)
    {
        var errors = new List<ValidationEntry>();
        var warnings = new List<ValidationEntry>();

        foreach (var script in configuration.Scripts)
        {
            var result = _scriptCompiler.TryCompile(script.Code, script.CanonicalName);
            if (!result.IsSuccess)
            {
                errors.Add(ValidationEntry.Error(ValidationCategory.ScriptCompilation,
                    $"Script '{script.CanonicalName}' failed compilation: {result.Error}",
                    script.CanonicalName));
            }
        }

        return new ValidationResult { Errors = errors, Warnings = warnings };
    }

    /// <summary>
    /// Validates that alarm trigger configurations reference existing attributes.
    /// Alarm trigger configs are JSON with an "attributeName" field referencing a canonical attribute name.
    /// </summary>
    public static ValidationResult ValidateAlarmTriggerReferences(FlattenedConfiguration configuration)
    {
        var errors = new List<ValidationEntry>();
        var attributeNames = new HashSet<string>(
            configuration.Attributes.Select(a => a.CanonicalName), StringComparer.Ordinal);

        foreach (var alarm in configuration.Alarms)
        {
            if (string.IsNullOrWhiteSpace(alarm.TriggerConfiguration))
                continue;

            var attrName = ExtractAttributeNameFromTriggerConfig(alarm.TriggerConfiguration);
            if (attrName != null && !attributeNames.Contains(attrName))
            {
                errors.Add(ValidationEntry.Error(ValidationCategory.AlarmTriggerReference,
                    $"Alarm '{alarm.CanonicalName}' references attribute '{attrName}' which does not exist in the flattened configuration.",
                    alarm.CanonicalName));
            }
        }

        return errors.Count > 0
            ? new ValidationResult { Errors = errors }
            : ValidationResult.Success();
    }

    /// <summary>
    /// Validates that script trigger configurations reference existing attributes.
    /// </summary>
    public static ValidationResult ValidateScriptTriggerReferences(FlattenedConfiguration configuration)
    {
        var errors = new List<ValidationEntry>();
        var attributeNames = new HashSet<string>(
            configuration.Attributes.Select(a => a.CanonicalName), StringComparer.Ordinal);

        foreach (var script in configuration.Scripts)
        {
            if (string.IsNullOrWhiteSpace(script.TriggerConfiguration))
                continue;

            var attrName = ExtractAttributeNameFromTriggerConfig(script.TriggerConfiguration);
            if (attrName != null && !attributeNames.Contains(attrName))
            {
                errors.Add(ValidationEntry.Error(ValidationCategory.ScriptTriggerReference,
                    $"Script '{script.CanonicalName}' trigger references attribute '{attrName}' which does not exist in the flattened configuration.",
                    script.CanonicalName));
            }
        }

        return errors.Count > 0
            ? new ValidationResult { Errors = errors }
            : ValidationResult.Success();
    }

    /// <summary>
    /// Validates that all data-sourced attributes have connection bindings.
    /// </summary>
    public static ValidationResult ValidateConnectionBindingCompleteness(FlattenedConfiguration configuration)
    {
        var errors = new List<ValidationEntry>();
        var warnings = new List<ValidationEntry>();

        foreach (var attr in configuration.Attributes)
        {
            if (attr.DataSourceReference != null && attr.BoundDataConnectionId == null)
            {
                warnings.Add(ValidationEntry.Warning(ValidationCategory.ConnectionBinding,
                    $"Attribute '{attr.CanonicalName}' has a data source reference but no connection binding.",
                    attr.CanonicalName));
            }
        }

        return new ValidationResult { Errors = errors, Warnings = warnings };
    }

    private static void CheckDuplicates<T>(
        IReadOnlyList<T> items,
        Func<T, string> getName,
        string entityType,
        List<ValidationEntry> errors)
    {
        var seen = new HashSet<string>(StringComparer.Ordinal);
        foreach (var item in items)
        {
            var name = getName(item);
            if (!seen.Add(name))
            {
                errors.Add(ValidationEntry.Error(ValidationCategory.NamingCollision,
                    $"{entityType} naming collision: '{name}' appears more than once.",
                    name));
            }
        }
    }

    internal static string? ExtractAttributeNameFromTriggerConfig(string triggerConfigJson)
    {
        try
        {
            using var doc = JsonDocument.Parse(triggerConfigJson);
            if (doc.RootElement.TryGetProperty("attributeName", out var prop))
                return prop.GetString();
        }
        catch (JsonException)
        {
            // Not valid JSON, ignore
        }
        return null;
    }
}
