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);
        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 position in LevelTraversal.AllPositions(level))
        {
            if (level.GetProp(position).Type != EPropType.Door)
                continue;

            if (!level.IsFloor(position))
            {
                errors.Add(new("Door prop must be placed on a floor cell.", position));
                continue;
            }

            var northSouthWalls = IsWall(level, new(position.X, position.Y - 1)) && IsWall(level, new(position.X, position.Y + 1));
            var westEastWalls = IsWall(level, new(position.X - 1, position.Y)) && IsWall(level, new(position.X + 1, position.Y));
            if (northSouthWalls == westEastWalls)
                errors.Add(new("Door must be surrounded by one opposing pair of wall cells.", position));
        }
    }

    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 control position must point to a reactor control prop.", reactor.ControlPosition));

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

        if (level.RequiredFuelConsumers < 0 || level.RequiredCoolantConsumers < 0 || level.RequiredElectricityConsumers < 0)
            errors.Add(new("Required consumer counts cannot be negative."));
    }

    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 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)
                    continue;

                var hasPresentNetwork = false;
                foreach (var carrier in Enum.GetValues<ECarrierType>())
                {
                    if (!level.GetUnderground(position, carrier).IsPresent)
                        continue;

                    hasPresentNetwork = true;
                    if (!HasSourcePath(level, position, carrier))
                        warnings.Add(new($"Enabled consumer has no {carrier} source path.", position));
                }

                if (!hasPresentNetwork)
                    warnings.Add(new("Enabled consumer has no underground network beneath it.", position));
            }
        }
    }

    private static bool IsWall(LevelState level, GridPosition position)
    {
        return level.InBounds(position) && level.GetTerrain(position) == ECellTerrain.Wall;
    }

    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;
    }
}