package p.studio.workspaces.assets.scene;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.dtos.PackerDiagnosticDTO;
import p.packer.messages.ListAssetsRequest;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.studio.Container;
import p.studio.projects.ProjectReference;
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
import p.studio.workspaces.assets.metadata.AssetStudioSceneTile;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public final class SceneBankWorkflowService {
    public static final String ACCEPTED_REQUEST_FILE = "scene-bank.pack-request.json";

    private final ObjectMapper mapper = new ObjectMapper();
    private final SceneBankGlyphAssetResolver glyphAssetResolver;

    public SceneBankWorkflowService() {
        this((projectReference, tilesetAssetRoot) -> Container.packer().workspaceService()
                .listAssets(new ListAssetsRequest(projectReference.toPackerProjectContext()))
                .assets()
                .stream()
                .filter(asset -> asset.assetFamily() == AssetFamilyCatalog.GLYPH_BANK)
                .filter(asset -> projectReference.rootPath()
                        .resolve("assets")
                        .resolve(tilesetAssetRoot)
                        .toAbsolutePath()
                        .normalize()
                        .equals(asset.identity().assetRoot().toAbsolutePath().normalize()))
                .findFirst()
                .map(asset -> asset.identity().assetId() == null ? -1 : asset.identity().assetId())
                .orElse(-1));
    }

    SceneBankWorkflowService(SceneBankGlyphAssetResolver glyphAssetResolver) {
        this.glyphAssetResolver = Objects.requireNonNull(glyphAssetResolver, "glyphAssetResolver");
    }

    public SceneBankWorkflowResult inspect(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
            return new SceneBankWorkflowResult(true, "", AssetWorkspaceSceneBankValidation.NOT_APPLICABLE, List.of());
        }
        final List<PackerDiagnosticDTO> diagnostics = new ArrayList<>(validateDiagnostics(projectReference, details));
        if (diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking)) {
            try {
                writeAcceptedRequest(
                        details.summary().assetRoot().resolve(ACCEPTED_REQUEST_FILE),
                        buildCanonicalRequest(projectReference, details));
            } catch (IOException | IllegalArgumentException exception) {
                diagnostics.add(error(
                        "Unable to persist canonical scene-pack request: " + exception.getMessage(),
                        details.summary().assetRoot().resolve(ACCEPTED_REQUEST_FILE)));
            }
        }
        final AssetWorkspaceSceneBankValidation validation = validationState(diagnostics);
        return new SceneBankWorkflowResult(
                diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
                messageFor(validation),
                validation,
                List.copyOf(diagnostics));
    }

    public SceneBankWorkflowResult validate(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        return inspect(projectReference, details);
    }

    public SceneBankWorkflowResult accept(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        return inspect(projectReference, details);
    }

    public SceneBankWorkflowResult inspectLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
        return inspect(projectReference, details);
    }

    public SceneBankWorkflowResult validateLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
        return inspect(projectReference, details);
    }

    public SceneBankWorkflowResult acceptLayer(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
        return inspect(projectReference, details);
    }

    private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
        final List<PackerDiagnosticDTO> diagnostics = new ArrayList<>();
        final AssetStudioSceneBankMetadata metadata = details.sceneBankMetadata();
        if (metadata == null) {
            diagnostics.add(error("Scene Bank support metadata is missing or invalid.",
                    details.summary().assetRoot().resolve("scene-bank.studio.json")));
            return diagnostics;
        }
        final SceneLayerRuntimeDefaults sceneDefaults =
                resolveSceneLayerRuntimeDefaults(projectReference, metadata.layerBindings());
        for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
            if (!binding.enabled()) {
                if (resolveLayerRuntimeDefaults(projectReference, binding, sceneDefaults) == null) {
                    diagnostics.add(error(
                            "Unable to resolve runtime glyph/tile_size defaults for inactive layer " + binding.index() + ".",
                            metadata.supportFile()));
                }
                continue;
            }
            if (binding.tilesetAssetRoot().isBlank()) {
                diagnostics.add(error("Layer " + binding.index() + " is missing tileset_asset_root.", metadata.supportFile()));
                continue;
            }
            if (binding.tiles().size() != binding.width() * binding.height()) {
                diagnostics.add(error("Canonical scene tile count does not match width * height for layer " + binding.index() + ".",
                        metadata.supportFile()));
                continue;
            }
            if (resolveGlyphAssetId(projectReference, binding.tilesetAssetRoot()) < 0) {
                diagnostics.add(error("Unable to resolve glyph_asset_id for layer " + binding.index() + ": " + binding.tilesetAssetRoot(),
                        metadata.supportFile()));
            }
            if (resolveTileSize(projectReference, binding.tilesetAssetRoot()) <= 0) {
                diagnostics.add(error("Unable to resolve tile_size for layer " + binding.index() + ": " + binding.tilesetAssetRoot(),
                        metadata.supportFile()));
            }
            for (AssetStudioSceneTile tile : binding.tiles()) {
                if (tile.glyphId() < 0) {
                    diagnostics.add(error("Canonical scene tile glyph_id must be non-negative for layer " + binding.index() + ".",
                            metadata.supportFile()));
                    break;
                }
                if (tile.paletteId() < 0) {
                    diagnostics.add(error("Canonical scene tile palette_id must be non-negative for layer " + binding.index() + ".",
                            metadata.supportFile()));
                    break;
                }
            }
        }
        return List.copyOf(diagnostics);
    }

    private ScenePackRequest buildCanonicalRequest(ProjectReference projectReference, AssetWorkspaceAssetDetails details) throws IOException {
        final AssetStudioSceneBankMetadata metadata = Objects.requireNonNull(details.sceneBankMetadata(), "sceneBankMetadata");
        final SceneLayerRuntimeDefaults sceneDefaults =
                resolveSceneLayerRuntimeDefaults(projectReference, metadata.layerBindings());
        final List<ScenePackLayerRequest> layers = new ArrayList<>();
        for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
            if (!binding.enabled()) {
                layers.add(inactiveLayer(projectReference, binding, sceneDefaults));
                continue;
            }
            layers.add(activeLayer(projectReference, binding));
        }
        return new ScenePackRequest(1, layers);
    }

    private ScenePackLayerRequest inactiveLayer(ProjectReference projectReference,
                                                AssetStudioSceneLayerBinding binding,
                                                SceneLayerRuntimeDefaults sceneDefaults) {
        final SceneLayerRuntimeDefaults defaults = resolveLayerRuntimeDefaults(projectReference, binding, sceneDefaults);
        if (defaults == null) {
            throw new IllegalArgumentException("Unable to resolve runtime defaults for inactive layer " + binding.index());
        }
        final List<ScenePackTileRequest> tiles = new ArrayList<>(binding.tiles().size());
        for (AssetStudioSceneTile tile : binding.tiles()) {
            tiles.add(new ScenePackTileRequest(false, 0, tile.paletteId(), false, false));
        }
        return new ScenePackLayerRequest(
                false,
                defaults.glyphAssetId(),
                defaults.tileSize(),
                binding.parallaxFactorX(),
                binding.parallaxFactorY(),
                binding.width(),
                binding.height(),
                tiles);
    }

    private ScenePackLayerRequest activeLayer(ProjectReference projectReference, AssetStudioSceneLayerBinding binding) throws IOException {
        final int glyphAssetId = resolveGlyphAssetId(projectReference, binding.tilesetAssetRoot());
        if (glyphAssetId < 0) {
            throw new IllegalArgumentException("Unable to resolve glyph_asset_id for " + binding.tilesetAssetRoot());
        }
        final int tileSize = resolveTileSize(projectReference, binding.tilesetAssetRoot());
        if (tileSize <= 0) {
            throw new IllegalArgumentException("Unable to resolve tile_size for " + binding.tilesetAssetRoot());
        }
        final List<ScenePackTileRequest> tiles = new ArrayList<>(binding.tiles().size());
        for (AssetStudioSceneTile tile : binding.tiles()) {
            if (!tile.active()) {
                tiles.add(new ScenePackTileRequest(false, 0, tile.paletteId(), false, false));
                continue;
            }
            tiles.add(new ScenePackTileRequest(true, tile.glyphId(), tile.paletteId(), tile.flipX(), tile.flipY()));
        }
        return new ScenePackLayerRequest(
                true,
                glyphAssetId,
                tileSize,
                binding.parallaxFactorX(),
                binding.parallaxFactorY(),
                binding.width(),
                binding.height(),
                tiles);
    }

    private SceneLayerRuntimeDefaults resolveSceneLayerRuntimeDefaults(ProjectReference projectReference,
                                                                      List<AssetStudioSceneLayerBinding> bindings) {
        for (AssetStudioSceneLayerBinding binding : bindings) {
            final SceneLayerRuntimeDefaults defaults = resolveLayerOwnRuntimeDefaults(projectReference, binding);
            if (defaults != null) {
                return defaults;
            }
        }
        return null;
    }

    private SceneLayerRuntimeDefaults resolveLayerRuntimeDefaults(ProjectReference projectReference,
                                                                 AssetStudioSceneLayerBinding binding,
                                                                 SceneLayerRuntimeDefaults sceneDefaults) {
        final SceneLayerRuntimeDefaults ownDefaults = resolveLayerOwnRuntimeDefaults(projectReference, binding);
        return ownDefaults != null ? ownDefaults : sceneDefaults;
    }

    private SceneLayerRuntimeDefaults resolveLayerOwnRuntimeDefaults(ProjectReference projectReference,
                                                                    AssetStudioSceneLayerBinding binding) {
        if (binding.tilesetAssetRoot().isBlank()) {
            return null;
        }
        final int glyphAssetId = resolveGlyphAssetId(projectReference, binding.tilesetAssetRoot());
        final int tileSize = resolveTileSize(projectReference, binding.tilesetAssetRoot());
        if (glyphAssetId < 0 || tileSize <= 0) {
            return null;
        }
        return new SceneLayerRuntimeDefaults(glyphAssetId, tileSize);
    }

    private void writeAcceptedRequest(Path path, ScenePackRequest request) throws IOException {
        final var root = mapper.createObjectNode();
        root.put("schema_version", 1);
        root.put("format", "SCENE/tilemap_v1");
        root.put("version", request.version());
        final var layersNode = root.putArray("layers");
        for (ScenePackLayerRequest layer : request.layers()) {
            final var layerNode = layersNode.addObject();
            layerNode.put("active", layer.active());
            layerNode.put("glyph_asset_id", layer.glyphAssetId());
            layerNode.put("tile_size", layer.tileSize());
            layerNode.put("parallax_factor_x", layer.parallaxFactorX());
            layerNode.put("parallax_factor_y", layer.parallaxFactorY());
            layerNode.put("width", layer.width());
            layerNode.put("height", layer.height());
            final var tilesNode = layerNode.putArray("tiles");
            for (ScenePackTileRequest tile : layer.tiles()) {
                final var tileNode = tilesNode.addObject();
                tileNode.put("active", tile.active());
                tileNode.put("glyph_id", tile.glyphId());
                tileNode.put("palette_id", tile.paletteId());
                tileNode.put("flip_x", tile.flipX());
                tileNode.put("flip_y", tile.flipY());
            }
        }
        mapper.writerWithDefaultPrettyPrinter().writeValue(path.toFile(), root);
    }

    private int resolveGlyphAssetId(ProjectReference projectReference, String tilesetAssetRoot) {
        try {
            return glyphAssetResolver.resolveGlyphAssetId(projectReference, tilesetAssetRoot);
        } catch (IOException exception) {
            return -1;
        }
    }

    private int resolveTileSize(ProjectReference projectReference, String tilesetAssetRoot) {
        final Path declarationPath = projectReference.rootPath()
                .resolve("assets")
                .resolve(tilesetAssetRoot)
                .resolve("asset.json")
                .toAbsolutePath()
                .normalize();
        if (!Files.isRegularFile(declarationPath)) {
            return -1;
        }
        try {
            final JsonNode root = mapper.readTree(declarationPath.toFile());
            final String value = root.path("output").path("metadata").path("tile_size").asText("").trim().toLowerCase();
            return switch (value) {
                case "8x8" -> 8;
                case "16x16" -> 16;
                case "32x32" -> 32;
                default -> -1;
            };
        } catch (IOException exception) {
            return -1;
        }
    }

    private AssetWorkspaceSceneBankValidation validationState(List<PackerDiagnosticDTO> diagnostics) {
        if (diagnostics.stream().anyMatch(PackerDiagnosticDTO::blocking)) {
            return new AssetWorkspaceSceneBankValidation(
                    AssetWorkspaceSceneBankStatus.VALIDATION_FAILED,
                    false,
                    false,
                    "",
                    "",
                    "");
        }
        return new AssetWorkspaceSceneBankValidation(
                AssetWorkspaceSceneBankStatus.READY,
                false,
                false,
                "",
                "",
                "");
    }

    private String messageFor(AssetWorkspaceSceneBankValidation validation) {
        return switch (validation.status()) {
            case NOT_APPLICABLE -> "";
            case PENDING_VALIDATION, VALIDATED_PENDING_ACCEPTANCE -> "Scene Bank canonical workflow no longer uses validation/acceptance staging.";
            case READY -> "Scene Bank canonical scene is ready.";
            case VALIDATION_FAILED -> "Scene Bank validation failed.";
        };
    }

    private PackerDiagnosticDTO error(String message, Path path) {
        return new PackerDiagnosticDTO(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                message,
                path,
                true);
    }

    @FunctionalInterface
    interface SceneBankGlyphAssetResolver {
        int resolveGlyphAssetId(ProjectReference projectReference, String tilesetAssetRoot) throws IOException;
    }

    private record SceneLayerRuntimeDefaults(int glyphAssetId, int tileSize) {
    }
}
