package p.studio.lsp.services.compiler;

import p.studio.compiler.messages.BuilderPipelineConfig;
import p.studio.compiler.messages.BuildingIssue;
import p.studio.compiler.models.AnalysisSnapshot;
import p.studio.compiler.models.BuilderPipelineContext;
import p.studio.compiler.source.identifiers.FileId;
import p.studio.compiler.utilities.SourceProviderFactory;
import p.studio.compiler.workspaces.BuilderPipelineService;
import p.studio.lsp.messages.*;
import p.studio.lsp.services.LanguageServiceBridge;
import p.studio.utilities.logs.LogAggregator;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public record CompilerLanguageServiceBridge() implements LanguageServiceBridge {

    @Override
    public BaselineServerDescription describeServer(final LspProjectContext context) {
        Objects.requireNonNull(context, "context");
        return new BaselineServerDescription(
                "Prometeu Studio LSP",
                "0.1.0",
                true);
    }

    @Override
    public BaselineDocumentAnalysis analyzeDocument(
            final LspProjectContext context,
            final String documentUri,
            final String text) {
        Objects.requireNonNull(context, "context");
        final Path documentPath = normalizeDocumentPath(documentUri);
        final String effectiveText = text == null ? "" : text;
        final AnalysisSnapshot snapshot = analyzeProject(context, Map.of(documentPath, effectiveText));
        return new BaselineDocumentAnalysis(mapDiagnostics(snapshot, documentPath, effectiveText));
    }

    @Override
    public BaselineHover hover(
            final LspProjectContext context,
            final String documentUri,
            final int line,
            final int character) {
        Objects.requireNonNull(context, "context");
        return new BaselineHover("""
                **Prometeu Studio LSP**
                
                Compiler-backed baseline.
                
                Semantic enrichment will be layered later.
                """);
    }

    @Override
    public String onSave(
            final LspProjectContext context,
            final String documentUri) {
        Objects.requireNonNull(context, "context");
        return "Prometeu Studio saw save: " + documentUri;
    }

    private AnalysisSnapshot analyzeProject(
            final LspProjectContext context,
            final Map<Path, String> overlays) {
        final BuilderPipelineContext pipelineContext = BuilderPipelineContext.fromConfig(
                new BuilderPipelineConfig(
                        false,
                        context.projectRoot().toString(),
                        "core-v1",
                        SourceProviderFactory.overlayUtf8(overlays)));
        return BuilderPipelineService.INSTANCE.analyze(pipelineContext, LogAggregator.empty());
    }

    private List<BaselineDocumentIssue> mapDiagnostics(
            final AnalysisSnapshot snapshot,
            final Path documentPath,
            final String text) {
        final ArrayList<BaselineDocumentIssue> issues = new ArrayList<>();
        final DocumentPositionMapper positionMapper = new DocumentPositionMapper(text);
        for (final BuildingIssue diagnostic : snapshot.diagnostics()) {
            if (diagnostic.getFileId() == null || diagnostic.getFileId() < 0) {
                continue;
            }
            final int fileIndex = diagnostic.getFileId();
            if (fileIndex >= snapshot.fileTable().size()) {
                continue;
            }
            final var sourceHandle = snapshot.fileTable().get(new FileId(fileIndex));
            if (!sourceHandle.getCanonPath().toAbsolutePath().normalize().equals(documentPath)) {
                continue;
            }
            issues.add(toBaselineIssue(diagnostic, positionMapper));
        }
        return List.copyOf(issues);
    }

    private BaselineDocumentIssue toBaselineIssue(
            final BuildingIssue diagnostic,
            final DocumentPositionMapper positionMapper) {
        final int startOffset = diagnostic.getStart() == null ? 0 : diagnostic.getStart();
        final int endOffset = diagnostic.getEnd() == null ? startOffset : Math.max(startOffset, diagnostic.getEnd());
        final DocumentPosition start = positionMapper.positionOf(startOffset);
        final DocumentPosition end = positionMapper.positionOf(endOffset);
        return new BaselineDocumentIssue(
                start.line(),
                start.character(),
                end.line(),
                end.character(),
                diagnostic.isError() ? BaselineIssueSeverity.ERROR : BaselineIssueSeverity.WARNING,
                diagnostic.getCode() == null || diagnostic.getCode().isBlank() ? "Prometeu Studio" : diagnostic.getCode(),
                diagnostic.getMessage());
    }

    private Path normalizeDocumentPath(final String documentUri) {
        return Path.of(URI.create(Objects.requireNonNull(documentUri, "documentUri"))).toAbsolutePath().normalize();
    }

    private record DocumentPosition(
            int line,
            int character) {
    }

    private static final class DocumentPositionMapper {
        private final String text;

        private DocumentPositionMapper(final String text) {
            this.text = Objects.requireNonNull(text, "text");
        }

        private DocumentPosition positionOf(final Integer byteOffset) {
            if (byteOffset == null || byteOffset <= 0 || text.isEmpty()) {
                return new DocumentPosition(0, 0);
            }
            final int target = Math.min(byteOffset, text.getBytes(StandardCharsets.UTF_8).length);
            int utf8Offset = 0;
            int line = 0;
            int character = 0;
            for (int index = 0; index < text.length(); ) {
                if (utf8Offset >= target) {
                    return new DocumentPosition(line, character);
                }
                final int codePoint = text.codePointAt(index);
                final int utf16Width = Character.charCount(codePoint);
                final int utf8Width = utf8Length(codePoint);
                if (utf8Offset + utf8Width > target) {
                    return new DocumentPosition(line, character);
                }
                utf8Offset += utf8Width;
                if (codePoint == '\n') {
                    line++;
                    character = 0;
                } else {
                    character += utf16Width;
                }
                index += utf16Width;
            }
            return new DocumentPosition(line, character);
        }

        private int utf8Length(final int codePoint) {
            if (codePoint <= 0x7F) {
                return 1;
            }
            if (codePoint <= 0x7FF) {
                return 2;
            }
            if (codePoint <= 0xFFFF) {
                return 3;
            }
            return 4;
        }
    }
}
