namespace ReactorMaintenance.Simulation;

public sealed class LevelValidator
{
    public ValidationReport Validate(LevelState level)
    {
        var errors = new List<ValidationIssue>();
        var warnings = new List<ValidationIssue>();

        ValidateDimensions(level, errors);
        ValidateRobot(level, errors);
        ValidateCells(level, errors);
        ValidateDoors(level, errors);
        ValidateLeaks(level, errors);
        ValidateReactors(level, errors, warnings);
        ValidateJunctions(level, errors);
        ValidateRuleEvents(level, errors);
        ValidateWarnings(level, warnings);

        return new() { Errors = errors, Warnings = warnings };
    }

    private static void ValidateDimensions(LevelState level, List<ValidationIssue> errors)
    {
        if (level.Width < Balancing.Current.MinimumLevelSize || level.Height < Balancing.Current.MinimumLevelSize)
            errors.Add(new("Invalid level dimensions."));

        var expected = level.Width * level.Height;
        if (level.Terrain.Length != expected || level.Fuel.Length != expected || level.Coolant.Length != expected || level.Electricity.Length != expected || level.Surface.Length != expected || level.Props.Length != expected)
            errors.Add(new("Cell array counts do not match level dimensions."));
    }

    private static void ValidateRobot(LevelState level, List<ValidationIssue> errors)
    {
        if (!level.IsFloor(level.Robot.Position))
            errors.Add(new("Robot must be in bounds on a floor cell.", level.Robot.Position));
    }

    private static void ValidateCells(LevelState level, List<ValidationIssue> errors)
    {
        for (var y = 0; y < level.Height; y++)
        {
            for (var x = 0; x < level.Width; x++)
            {
                var position = new GridPosition(x, y);
                var surface = level.GetSurface(position);
                var prop = level.GetProp(position);

                if (level.GetTerrain(position) == ECellTerrain.Wall)
                {
                    if (surface.Fuel > 0 || surface.Coolant > 0 || surface.Electricity > 0 || surface.Heat > 0)
                        errors.Add(new("Wall cell cannot store surface hazards.", position));

                    if (prop.Type != EPropType.None)
                        errors.Add(new("Prop must be placed on floor terrain.", position));
                }
            }
        }
    }

    private static void ValidateDoors(LevelState level, List<ValidationIssue> errors)
    {
        foreach (var door in level.Doors)
        {
            if (!level.IsFloor(door.A) || !level.IsFloor(door.B) || door.A.ManhattanDistance(door.B) != 1)
                errors.Add(new("Door edge must connect two adjacent floor cells.", door.A));
        }
    }

    private static void ValidateLeaks(LevelState level, List<ValidationIssue> errors)
    {
        foreach (var leak in level.Leaks)
        {
            if (!level.InBounds(leak.UndergroundPosition) || !level.IsFloor(leak.AccessPosition))
            {
                errors.Add(new("Leak must have valid floor access.", leak.AccessPosition));
                continue;
            }

            var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
            if (!underground.IsPresent)
                errors.Add(new("Leak target must point to an underground cell.", leak.UndergroundPosition));

            if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && leak.UndergroundPosition != leak.AccessPosition)
                errors.Add(new("Fuel and coolant leaks must use their underground coordinate as access.", leak.AccessPosition));

            if (leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition.ManhattanDistance(leak.AccessPosition) != 1)
                errors.Add(new("Electricity leak access must be an adjacent floor face.", leak.AccessPosition));
        }
    }

    private static void ValidateReactors(LevelState level, List<ValidationIssue> errors, List<ValidationIssue> warnings)
    {
        foreach (var reactor in level.Reactors)
        {
            if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
                errors.Add(new("Reactor binding control position must point to a reactor control prop.", reactor.ControlPosition));

            ValidateConsumerBinding(level, reactor.FuelConsumerPosition, ECarrierType.Fuel, errors);
            ValidateConsumerBinding(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant, errors);
            ValidateConsumerBinding(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity, errors);

            if (!reactor.Ready)
                warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
        }
    }

    private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List<ValidationIssue> errors)
    {
        if (!level.InBounds(position) || level.GetProp(position) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
            errors.Add(new($"Missing or invalid {carrier} consumer binding.", position));
    }

    private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
    {
        foreach (var junction in JunctionFlowAnalyzer.Analyze(level))
            errors.AddRange(junction.Errors.Select(error => new ValidationIssue(error, junction.Position)));
    }

    private static void ValidateRuleEvents(LevelState level, List<ValidationIssue> errors)
    {
        foreach (var ruleEvent in level.RuleEvents)
        {
            foreach (var predicate in ruleEvent.Predicates)
                ValidateRulePredicate(level, predicate, errors);

            foreach (var effect in ruleEvent.Effects)
                ValidateRuleEffect(level, effect, errors);
        }
    }

    private static void ValidateRulePredicate(LevelState level, RulePredicate predicate, List<ValidationIssue> errors)
    {
        switch (predicate.Kind)
        {
            case ERulePredicateKind.PropStateAt:
                if (!level.InBounds(predicate.Position) || level.GetProp(predicate.Position).Type == EPropType.None)
                    errors.Add(new("Rule prop predicate must target a prop.", predicate.Position));
                break;
            case ERulePredicateKind.ConsumerStateAt:
                if (!IsProp(level, predicate.Position, EPropType.Consumer))
                    errors.Add(new("Rule consumer predicate must target a consumer prop.", predicate.Position));
                break;
            case ERulePredicateKind.NetworkBandAt:
                if (!level.InBounds(predicate.Position) || !level.GetUnderground(predicate.Position, predicate.Carrier).IsPresent)
                    errors.Add(new("Rule network predicate must target an underground cell.", predicate.Position));
                break;
            case ERulePredicateKind.SurfaceBandAt:
            case ERulePredicateKind.RobotAt:
                if (!level.IsFloor(predicate.Position))
                    errors.Add(new("Rule floor predicate must target a floor cell.", predicate.Position));
                break;
            case ERulePredicateKind.ReactorReadyIs:
            case ERulePredicateKind.ReactorWonIs:
                ValidateOptionalReactorId(level, predicate.ReactorId, predicate.Position, errors);
                break;
        }
    }

    private static void ValidateRuleEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
    {
        if (RequiresNonNegativeAmount(effect.Kind) && effect.Amount < 0)
            errors.Add(new("Rule effect amount must be non-negative.", effect.Position));

        switch (effect.Kind)
        {
            case ERuleEffectKind.StartLeak:
                ValidateRuleLeakEffect(level, effect, errors);
                break;
            case ERuleEffectKind.WorsenLeak:
            case ERuleEffectKind.RepairNetworkCell:
            case ERuleEffectKind.DisableNetworkCell:
                if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
                    errors.Add(new("Rule network effect must target an underground cell.", effect.Position));
                break;
            case ERuleEffectKind.SetPropEnabled:
                if (!level.InBounds(effect.Position) || level.GetProp(effect.Position).Type == EPropType.None)
                    errors.Add(new("Rule prop effect must target a prop.", effect.Position));
                break;
            case ERuleEffectKind.AddSurfaceHazard:
            case ERuleEffectKind.RemoveSurfaceHazard:
            case ERuleEffectKind.AddHeat:
            case ERuleEffectKind.RemoveHeat:
                if (!level.IsFloor(effect.Position))
                    errors.Add(new("Rule surface effect must target a floor cell.", effect.Position));
                break;
        }
    }

    private static bool RequiresNonNegativeAmount(ERuleEffectKind kind)
    {
        return kind is ERuleEffectKind.AddSurfaceHazard
            or ERuleEffectKind.RemoveSurfaceHazard
            or ERuleEffectKind.AddHeat
            or ERuleEffectKind.RemoveHeat
            or ERuleEffectKind.AddInventory
            or ERuleEffectKind.RemoveInventory;
    }

    private static void ValidateRuleLeakEffect(LevelState level, RuleEffect effect, List<ValidationIssue> errors)
    {
        var accessPosition = effect.AccessPosition ?? effect.Position;
        if (!level.InBounds(effect.Position) || !level.GetUnderground(effect.Position, effect.Carrier).IsPresent)
            errors.Add(new("Rule leak effect must target an underground cell.", effect.Position));

        if (!level.IsFloor(accessPosition))
        {
            errors.Add(new("Rule leak effect must have valid floor access.", accessPosition));
            return;
        }

        if (effect.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && effect.Position != accessPosition)
            errors.Add(new("Rule fuel and coolant leak effects must use their underground coordinate as access.", accessPosition));

        if (effect.Carrier == ECarrierType.Electricity && effect.Position.ManhattanDistance(accessPosition) != 1)
            errors.Add(new("Rule electricity leak effect access must be an adjacent floor face.", accessPosition));
    }

    private static void ValidateOptionalReactorId(LevelState level, int reactorId, GridPosition position, List<ValidationIssue> errors)
    {
        if (reactorId > 0 && level.Reactors.All(reactor => reactor.ReactorId != reactorId))
            errors.Add(new("Rule reactor predicate must reference an existing reactor.", position));
    }

    private static void ValidateWarnings(LevelState level, List<ValidationIssue> warnings)
    {
        foreach (var carrier in Enum.GetValues<ECarrierType>())
        {
            for (var y = 0; y < level.Height; y++)
            {
                for (var x = 0; x < level.Width; x++)
                {
                    var position = new GridPosition(x, y);
                    if (level.GetUnderground(position, carrier).IsPresent && !HasSourcePath(level, position, carrier))
                        warnings.Add(new($"Underground {carrier} cell has no source path.", position));
                }
            }
        }

        for (var y = 0; y < level.Height; y++)
        {
            for (var x = 0; x < level.Width; x++)
            {
                var position = new GridPosition(x, y);
                var prop = level.GetProp(position);
                if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier))
                    warnings.Add(new("Enabled consumer is initially starved.", position));
            }
        }
    }

    private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier)
    {
        if (!level.GetUnderground(start, carrier).CarriesFlow)
            return false;

        var visited = new HashSet<GridPosition>();
        var open = new Queue<GridPosition>();
        open.Enqueue(start);
        visited.Add(start);

        while (open.Count > 0)
        {
            var current = open.Dequeue();
            if (level.GetProp(current) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
                return true;

            foreach (var next in current.Neighbors().Where(level.InBounds))
            {
                if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
                    continue;

                open.Enqueue(next);
            }
        }

        return false;
    }

    private static bool IsProp(LevelState level, GridPosition position, EPropType propType)
    {
        return level.InBounds(position) && level.GetProp(position).Type == propType;
    }
}