package p.packer.services;

import com.fasterxml.jackson.databind.ObjectMapper;
import p.packer.messages.PackerOperationStatus;
import p.packer.messages.PackerProjectContext;
import p.packer.messages.assets.AssetFamilyCatalog;
import p.packer.messages.AssetReference;
import p.packer.messages.GetAssetActionsRequest;
import p.packer.messages.GetAssetActionsResult;
import p.packer.messages.assets.OutputFormatCatalog;
import p.packer.messages.assets.PackerAssetState;
import p.packer.messages.assets.PackerBuildParticipation;
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.events.PackerEventKind;
import p.packer.events.PackerEventSink;
import p.packer.events.PackerProgress;
import p.packer.messages.CreateAssetRequest;
import p.packer.messages.CreateAssetResult;
import p.packer.messages.GetAssetDetailsRequest;
import p.packer.messages.GetAssetDetailsResult;
import p.packer.messages.InitWorkspaceRequest;
import p.packer.messages.InitWorkspaceResult;
import p.packer.messages.ListAssetsRequest;
import p.packer.messages.ListAssetsResult;
import p.packer.messages.RegisterAssetRequest;
import p.packer.messages.RegisterAssetResult;
import p.packer.PackerWorkspaceService;
import p.packer.models.PackerAssetDeclarationParseResult;
import p.packer.models.PackerAssetIdentity;
import p.packer.models.PackerRegisterAssetEvaluation;
import p.packer.models.PackerAssetSummary;
import p.packer.models.PackerDiagnostic;
import p.packer.models.PackerRegistryEntry;
import p.packer.models.PackerRegistryState;
import p.packer.models.PackerRuntimeAsset;
import p.packer.models.PackerRuntimeSnapshot;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.stream.Stream;

public final class FileSystemPackerWorkspaceService implements PackerWorkspaceService {
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private final PackerWorkspaceFoundation workspaceFoundation;
    private final PackerAssetDetailsService detailsService;
    private final PackerAssetActionReadService actionReadService;
    private final PackerRuntimePatchService runtimePatchService;
    private final PackerRuntimeRegistry runtimeRegistry;
    private final PackerProjectWriteCoordinator writeCoordinator;
    private final PackerEventSink eventSink;

    public FileSystemPackerWorkspaceService(
            PackerWorkspaceFoundation workspaceFoundation,
            PackerAssetDetailsService detailsService,
            PackerAssetActionReadService actionReadService,
            PackerRuntimePatchService runtimePatchService,
            PackerRuntimeRegistry runtimeRegistry,
            PackerProjectWriteCoordinator writeCoordinator,
            PackerEventSink eventSink) {
        this.workspaceFoundation = Objects.requireNonNull(workspaceFoundation, "workspaceFoundation");
        this.detailsService = Objects.requireNonNull(detailsService, "detailsService");
        this.actionReadService = Objects.requireNonNull(actionReadService, "actionReadService");
        this.runtimePatchService = Objects.requireNonNull(runtimePatchService, "runtimePatchService");
        this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry");
        this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
        this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
    }

    @Override
    public InitWorkspaceResult initWorkspace(InitWorkspaceRequest request) {
        final InitWorkspaceResult result = workspaceFoundation.initWorkspace(request);
        runtimeRegistry.refresh(Objects.requireNonNull(request, "request").project());
        return result;
    }

    @Override
    public ListAssetsResult listAssets(ListAssetsRequest request) {
        final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
        final PackerRegistryState registry = snapshot.registry();
        final Map<Path, PackerRegistryEntry> registryByRoot = new HashMap<>();
        for (PackerRegistryEntry entry : registry.assets()) {
            registryByRoot.put(PackerWorkspacePaths.assetRoot(project, entry.root()), entry);
        }

        final List<PackerAssetSummary> assets = new ArrayList<>();
        final List<PackerDiagnostic> diagnostics = new ArrayList<>();
        final Set<Path> discoveredRoots = new HashSet<>();

        final List<PackerRuntimeAsset> runtimeAssets = snapshot.assets();
        final int total = runtimeAssets.size();
        for (int index = 0; index < runtimeAssets.size(); index += 1) {
            final PackerRuntimeAsset runtimeAsset = runtimeAssets.get(index);
            final Path assetRoot = runtimeAsset.assetRoot();
            final Path assetManifestPath = runtimeAsset.manifestPath();
            discoveredRoots.add(assetRoot);
            final PackerRegistryEntry registryEntry = registryByRoot.get(assetRoot);
            final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration();
            diagnostics.addAll(parsed.diagnostics());
            diagnostics.addAll(identityMismatchDiagnostics(registryEntry, parsed, assetManifestPath));
            final PackerAssetSummary summary = buildSummary(project, assetRoot, registryEntry, parsed);
            assets.add(summary);
            events.emit(
                    PackerEventKind.ASSET_DISCOVERED,
                    "Discovered asset: " + summary.identity().assetName(),
                    new PackerProgress(total == 0 ? 1.0d : (index + 1) / (double) total, false),
                    List.of(summary.identity().assetName()));
        }

        for (PackerRegistryEntry entry : registry.assets()) {
            final Path registeredRoot = PackerWorkspacePaths.assetRoot(project, entry.root());
            if (!discoveredRoots.contains(registeredRoot)) {
                diagnostics.add(new PackerDiagnostic(
                        PackerDiagnosticSeverity.ERROR,
                        PackerDiagnosticCategory.STRUCTURAL,
                        "Registered asset root is missing asset.json: " + entry.root(),
                        registeredRoot.resolve("asset.json"),
                        true));
            }
        }

        assets.sort(Comparator
                .comparing((PackerAssetSummary asset) -> asset.identity().assetRoot().toString(), String.CASE_INSENSITIVE_ORDER)
                .thenComparing(summary -> summary.identity().assetName(), String.CASE_INSENSITIVE_ORDER));
        final PackerOperationStatus status = diagnostics.stream().anyMatch(PackerDiagnostic::blocking)
                ? PackerOperationStatus.PARTIAL
                : PackerOperationStatus.SUCCESS;
        if (!diagnostics.isEmpty()) {
            events.emit(PackerEventKind.DIAGNOSTICS_UPDATED, "Asset scan diagnostics updated.", List.of());
        }
        return new ListAssetsResult(
                status,
                "Packer asset snapshot ready.",
                PackerReadMessageMapper.toAssetSummaryDTOs(assets),
                PackerReadMessageMapper.toDiagnosticDTOs(diagnostics));
    }

    @Override
    public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
        return detailsService.getAssetDetails(request);
    }

    @Override
    public GetAssetActionsResult getAssetActions(GetAssetActionsRequest request) {
        return actionReadService.getAssetActions(request);
    }

    @Override
    public CreateAssetResult createAsset(CreateAssetRequest request) {
        final CreateAssetRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);

        return writeCoordinator.execute(project, () -> createAssetInWriteLane(safeRequest, events));
    }

    @Override
    public RegisterAssetResult registerAsset(RegisterAssetRequest request) {
        final RegisterAssetRequest safeRequest = Objects.requireNonNull(request, "request");
        final PackerProjectContext project = safeRequest.project();
        final PackerOperationEventEmitter events = new PackerOperationEventEmitter(project, eventSink);
        return writeCoordinator.execute(project, () -> registerAssetInWriteLane(safeRequest, events));
    }

    private CreateAssetResult createAssetInWriteLane(
            CreateAssetRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));

        final String relativeAssetRoot = normalizeRelativeAssetRoot(request.assetRoot());
        if (relativeAssetRoot == null) {
            return failureResult(events, "Asset root must stay inside assets/ and use a non-blank relative path.", null, null, List.of());
        }

        if (request.assetFamily() == AssetFamilyCatalog.UNKNOWN) {
            return failureResult(events, "Asset type is required.", null, null, List.of());
        }
        if (request.outputFormat() == OutputFormatCatalog.UNKNOWN) {
            return failureResult(events, "Output format is required.", null, null, List.of());
        }
        if (request.outputFormat().assetFamily() != request.assetFamily()) {
            return failureResult(events, "Output format is not supported for the selected asset type.", null, null, List.of(request.assetName()));
        }
        if (!request.outputFormat().supports(request.outputCodec())) {
            return failureResult(events, "Output codec is not supported for the selected output format.", null, null, List.of(request.assetName()));
        }

        final Path assetRoot = PackerWorkspacePaths.assetRoot(project, relativeAssetRoot);
        if (!assetRoot.startsWith(PackerWorkspacePaths.assetsRoot(project))) {
            return failureResult(events, "Asset root must stay inside assets/.", null, null, List.of());
        }

        final Path manifestPath = assetRoot.resolve("asset.json");
        try {
            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final boolean rootAlreadyRegistered = registry.assets().stream()
                    .map(entry -> PackerWorkspacePaths.assetRoot(project, entry.root()))
                    .anyMatch(assetRoot::equals);
            if (rootAlreadyRegistered) {
                return failureResult(events, "Asset root is already registered.", assetRoot, manifestPath, List.of(relativeAssetRoot));
            }

            if (Files.isRegularFile(manifestPath)) {
                return failureResult(events, "Asset root already contains asset.json.", assetRoot, manifestPath, List.of(relativeAssetRoot));
            }
            if (Files.isDirectory(assetRoot)) {
                try (Stream<Path> children = Files.list(assetRoot)) {
                    if (children.findAny().isPresent()) {
                        return failureResult(events, "Asset root already exists and is not empty.", assetRoot, manifestPath, List.of(relativeAssetRoot));
                    }
                }
            }

            final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
            Files.createDirectories(assetRoot);
            writeManifest(manifestPath, request, entry.assetUuid());
            final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
            workspaceFoundation.saveRegistry(project, updated);
            runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
                    snapshot,
                    generation,
                    updated,
                    entry,
                    assetRoot,
                    manifestPath));
            final CreateAssetResult result = new CreateAssetResult(
                    PackerOperationStatus.SUCCESS,
                    "Asset created: " + relativeAssetRoot,
                    AssetReference.forAssetId(entry.assetId()),
                    assetRoot,
                    manifestPath);
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
            return result;
        } catch (IOException exception) {
            return failureResult(
                    events,
                    "Unable to create asset: " + exception.getMessage(),
                    assetRoot,
                    manifestPath,
                    List.of(relativeAssetRoot));
        }
    }

    private RegisterAssetResult registerAssetInWriteLane(
            RegisterAssetRequest request,
            PackerOperationEventEmitter events) {
        final PackerProjectContext project = request.project();
        workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
        final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
        final PackerRegisterAssetEvaluation evaluation = actionReadService.evaluateRegister(snapshot, project, request.assetReference());
        final Path assetRoot = evaluation.resolved().assetRoot();
        final Path manifestPath = assetRoot.resolve("asset.json");
        final String relativeAssetRoot = relativeAssetRoot(project, assetRoot);
        if (!evaluation.canRegister()) {
            final String summary = Objects.requireNonNullElse(evaluation.reason(), "Asset cannot be registered.");
            return registerFailureResult(events, summary, assetRoot, manifestPath, List.of(relativeAssetRoot));
        }

        final var declaration = evaluation.resolved().runtimeAsset().orElseThrow().parsedDeclaration().declaration();
        try {
            final PackerRegistryState registry = workspaceFoundation.loadRegistry(project);
            final PackerRegistryEntry entry = workspaceFoundation.registerExistingAsset(
                    project,
                    registry,
                    assetRoot,
                    declaration.assetUuid());
            final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
            workspaceFoundation.saveRegistry(project, updated);
            runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterRegisterAsset(
                    currentSnapshot,
                    generation,
                    updated,
                    entry,
                    assetRoot));
            final RegisterAssetResult result = new RegisterAssetResult(
                    PackerOperationStatus.SUCCESS,
                    "Asset registered: " + relativeAssetRoot,
                    AssetReference.forAssetId(entry.assetId()),
                    assetRoot,
                    manifestPath);
            events.emit(PackerEventKind.ACTION_APPLIED, result.summary(), List.of(relativeAssetRoot));
            return result;
        } catch (RuntimeException exception) {
            return registerFailureResult(
                    events,
                    "Unable to register asset: " + exception.getMessage(),
                    assetRoot,
                    manifestPath,
                    List.of(relativeAssetRoot));
        }
    }

    private CreateAssetResult failureResult(
            PackerOperationEventEmitter events,
            String summary,
            Path assetRoot,
            Path manifestPath,
            List<String> affectedAssets) {
        final CreateAssetResult result = new CreateAssetResult(
                PackerOperationStatus.FAILED,
                summary,
                null,
                assetRoot,
                manifestPath);
        events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
        return result;
    }

    private RegisterAssetResult registerFailureResult(
            PackerOperationEventEmitter events,
            String summary,
            Path assetRoot,
            Path manifestPath,
            List<String> affectedAssets) {
        final RegisterAssetResult result = new RegisterAssetResult(
                PackerOperationStatus.FAILED,
                summary,
                null,
                assetRoot,
                manifestPath);
        events.emit(PackerEventKind.ACTION_FAILED, result.summary(), affectedAssets);
        return result;
    }

    private void writeManifest(Path manifestPath, CreateAssetRequest request, String assetUuid) throws IOException {
        final Map<String, Object> manifest = new LinkedHashMap<>();
        manifest.put("schema_version", 1);
        manifest.put("asset_uuid", assetUuid);
        manifest.put("name", request.assetName());
        manifest.put("type", request.assetFamily().manifestType());
        manifest.put("inputs", Map.of());
        manifest.put("output", Map.of(
                "format", request.outputFormat().manifestValue(),
                "codec", request.outputCodec().manifestValue()));
        manifest.put("preload", Map.of("enabled", request.preloadEnabled()));
        MAPPER.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
    }

    private String normalizeRelativeAssetRoot(String candidate) {
        final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
        if (raw.isBlank()) {
            return null;
        }
        final Path normalized = Path.of(raw).normalize();
        if (normalized.isAbsolute() || normalized.startsWith("..")) {
            return null;
        }
        final String value = normalized.toString().replace('\\', '/');
        return value.isBlank() ? null : value;
    }

    private PackerAssetSummary buildSummary(
            PackerProjectContext project,
            Path assetRoot,
            PackerRegistryEntry registryEntry,
            PackerAssetDeclarationParseResult parsed) {
        final String assetName = parsed.declaration() != null
                ? parsed.declaration().name()
                : assetRoot.getFileName().toString();
        final AssetFamilyCatalog assetFamily = parsed.declaration() != null
                ? parsed.declaration().assetFamily()
                : AssetFamilyCatalog.UNKNOWN;
        final boolean preload = parsed.declaration() != null && parsed.declaration().preloadEnabled();
        final boolean hasDiagnostics = !parsed.diagnostics().isEmpty();
        final PackerAssetState state = registryEntry == null
                ? PackerAssetState.UNREGISTERED
                : PackerAssetState.REGISTERED;
        final PackerBuildParticipation buildParticipation = state == PackerAssetState.REGISTERED
                ? (registryEntry.includedInBuild() ? PackerBuildParticipation.INCLUDED : PackerBuildParticipation.EXCLUDED)
                : PackerBuildParticipation.EXCLUDED;
        return new PackerAssetSummary(
                assetReferenceFor(project, assetRoot, registryEntry),
                new PackerAssetIdentity(
                        registryEntry == null ? null : registryEntry.assetId(),
                        registryEntry == null
                                ? parsed.declaration() == null ? null : parsed.declaration().assetUuid()
                                : registryEntry.assetUuid(),
                        assetName,
                        assetRoot),
                state,
                buildParticipation,
                assetFamily,
                preload,
                hasDiagnostics);
    }

    private List<PackerDiagnostic> identityMismatchDiagnostics(
            PackerRegistryEntry registryEntry,
            PackerAssetDeclarationParseResult parsed,
            Path manifestPath) {
        if (registryEntry == null || parsed.declaration() == null || registryEntry.assetUuid().equals(parsed.declaration().assetUuid())) {
            return List.of();
        }
        return List.of(new PackerDiagnostic(
                PackerDiagnosticSeverity.ERROR,
                PackerDiagnosticCategory.STRUCTURAL,
                "Field 'asset_uuid' does not match the registered asset identity.",
                manifestPath,
                true));
    }

    private AssetReference assetReferenceFor(
            PackerProjectContext project,
            Path assetRoot,
            PackerRegistryEntry registryEntry) {
        if (registryEntry != null) {
            return AssetReference.forAssetId(registryEntry.assetId());
        }
        return AssetReference.forRelativeAssetRoot(PackerWorkspacePaths.assetsRoot(project)
                .relativize(assetRoot.toAbsolutePath().normalize())
                .toString()
                .replace('\\', '/'));
    }

    private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) {
        return PackerWorkspacePaths.relativeAssetRoot(project, assetRoot).replace('\\', '/');
    }
}
