package p.studio.projects;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import p.studio.projectstate.ProjectLocalStudioSetup;
import p.studio.projectstate.ProjectLocalStudioSetupService;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.stream.Stream;

public final class ProjectCatalogService {
    private static final String MANIFEST_FILE_NAME = "prometeu.json";
    private static final ObjectMapper MAPPER = new ObjectMapper();
    private final Path projectsRoot;
    private final ProjectLocalStudioSetupService projectLocalStudioSetupService = new ProjectLocalStudioSetupService();

    public ProjectCatalogService(Path projectsRoot) {
        this.projectsRoot = Objects.requireNonNull(projectsRoot, "projectsRoot").toAbsolutePath().normalize();
    }

    public Path projectsRoot() {
        return projectsRoot;
    }

    public List<ProjectReference> listProjects() {
        if (!Files.isDirectory(projectsRoot)) {
            return List.of();
        }

        try (Stream<Path> children = Files.list(projectsRoot)) {
            return children
                    .filter(this::isValidProjectRoot)
                    .map(this::buildReference)
                    .sorted(Comparator.comparing(ProjectReference::name, String.CASE_INSENSITIVE_ORDER))
                    .toList();
        } catch (IOException ioException) {
            throw new UncheckedIOException(ioException);
        }
    }

    public ProjectReference openProject(Path projectPath) {
        final Path normalized = Objects.requireNonNull(projectPath, "projectPath").toAbsolutePath().normalize();
        return openProjectManifest(manifestPath(normalized));
    }

    public ProjectReference openProjectManifest(Path manifestPath) {
        final Path normalizedManifest = Objects.requireNonNull(manifestPath, "manifestPath")
                .toAbsolutePath()
                .normalize();
        if (!normalizedManifest.getFileName().toString().equals(MANIFEST_FILE_NAME)) {
            throw new IllegalArgumentException("expected " + MANIFEST_FILE_NAME + ": " + normalizedManifest);
        }
        if (!Files.isRegularFile(normalizedManifest)) {
            throw new IllegalArgumentException("project manifest not found: expected " + normalizedManifest);
        }

        final Path projectRoot = normalizedManifest.getParent();
        if (projectRoot == null || !Files.isDirectory(projectRoot)) {
            throw new IllegalArgumentException("project root does not exist: " + normalizedManifest);
        }

        return buildReference(projectRoot);
    }

    public ProjectReference createProject(String projectName) {
        return createProject(new ProjectCreationRequest(projectName, projectsRoot, "pbs", 1, "src", 4, null));
    }

    public ProjectReference createProject(String projectName, Path parentLocation) {
        return createProject(new ProjectCreationRequest(projectName, parentLocation, "pbs", 1, "src", 4, null));
    }

    public ProjectReference createProject(ProjectCreationRequest request) {
        final String displayName = Objects.requireNonNull(request.projectName(), "request.projectName").trim();
        final String sanitized = sanitizeProjectName(displayName);
        if (displayName.isBlank() || sanitized.isBlank()) {
            throw new IllegalArgumentException("project name must not be blank");
        }
        final String languageId = Objects.requireNonNull(request.languageId(), "request.languageId").trim();
        if (languageId.isBlank()) {
            throw new IllegalArgumentException("project language must not be blank");
        }
        if (request.stdlib() <= 0) {
            throw new IllegalArgumentException("project stdlib major must be positive");
        }
        if (request.indentationWidth() <= 0) {
            throw new IllegalArgumentException("project indentation width must be positive");
        }

        final Path sourceRoot = normalizeSourceRoot(request.sourceRoot());
        final Path normalizedParent = Objects.requireNonNull(request.parentLocation(), "request.parentLocation")
                .toAbsolutePath()
                .normalize();
        if (Files.exists(normalizedParent) && !Files.isDirectory(normalizedParent)) {
            throw new IllegalArgumentException("project location is not a directory: " + normalizedParent);
        }

        final Path projectRoot = normalizedParent.resolve(sanitized).normalize();
        if (Files.exists(projectRoot)) {
            throw new IllegalArgumentException("project already exists: " + sanitized);
        }

        try {
            Files.createDirectories(normalizedParent);
            Files.createDirectories(projectRoot.resolve(".workspace"));
            final ProjectReference projectReference = new ProjectReference(
                    displayName,
                    "1.0.0",
                    languageId,
                    request.stdlib(),
                    projectRoot);
            Files.createDirectories(ProjectStudioPaths.studioRoot(projectReference));
            Files.createDirectories(projectRoot.resolve(sourceRoot));
            Files.createDirectories(projectRoot.resolve("assets"));
            Files.createDirectories(projectRoot.resolve("build"));
            Files.createDirectories(projectRoot.resolve("cartridge"));
            Files.writeString(manifestPath(projectRoot), defaultManifest(displayName, languageId, request.stdlib()));
            Files.writeString(projectRoot.resolve(".gitignore"), defaultGitIgnore());
            projectLocalStudioSetupService.save(
                    projectReference,
                    new ProjectLocalStudioSetup(
                            request.runtimePath(),
                            new ProjectLocalStudioSetup.EditorIndentationSetup("spaces", request.indentationWidth())));
        } catch (IOException ioException) {
            throw new UncheckedIOException(ioException);
        }

        return openProject(projectRoot);
    }

    private ProjectReference buildReference(Path projectRoot) {
        final Path normalizedRoot = projectRoot.toAbsolutePath().normalize();
        final Path manifestPath = manifestPath(normalizedRoot);
        final ProjectManifestSummary manifest;
        try {
            manifest = MAPPER.readValue(manifestPath.toFile(), ProjectManifestSummary.class);
        } catch (IOException ioException) {
            throw new UncheckedIOException(ioException);
        }

        if (manifest.name() == null || manifest.name().isBlank()) {
            throw new IllegalArgumentException("project manifest missing name: " + manifestPath);
        }
        if (manifest.version() == null || manifest.version().isBlank()) {
            throw new IllegalArgumentException("project manifest missing version: " + manifestPath);
        }
        if (manifest.language() == null || manifest.language().isBlank()) {
            throw new IllegalArgumentException("project manifest missing language: " + manifestPath);
        }
        final int stdlibMajor;
        try {
            stdlibMajor = Integer.parseInt(Objects.requireNonNull(manifest.stdlib(), "manifest.stdlib").trim());
        } catch (RuntimeException runtimeException) {
            throw new IllegalArgumentException("project manifest invalid stdlib: " + manifestPath, runtimeException);
        }
        return new ProjectReference(
                manifest.name().trim(),
                manifest.version().trim(),
                manifest.language().trim(),
                stdlibMajor,
                normalizedRoot);
    }

    private boolean isValidProjectRoot(Path path) {
        return Files.isDirectory(path) && Files.isRegularFile(manifestPath(path));
    }

    private Path manifestPath(Path projectRoot) {
        return projectRoot.resolve(MANIFEST_FILE_NAME);
    }

    private Path normalizeSourceRoot(String sourceRoot) {
        final String raw = Objects.requireNonNull(sourceRoot, "sourceRoot").trim();
        if (raw.isBlank()) {
            throw new IllegalArgumentException("project source root must not be blank");
        }
        final Path normalized = Path.of(raw).normalize();
        if (normalized.isAbsolute() || normalized.startsWith("..")) {
            throw new IllegalArgumentException("project source root must be a relative project path");
        }
        return normalized;
    }

    private String defaultManifest(String projectName, String languageId, int stdlibMajor) {
        return """
                {
                  "name": "%s",
                  "version": "1.0.0",
                  "language": "%s",
                  "stdlib": "%d",
                  "dependencies": [
                  ]
                }
                """.formatted(projectName, languageId, stdlibMajor);
    }

    private String defaultGitIgnore() {
        return """
                .studio/
                .workspace/
                build/

                # macOS
                .DS_Store
                .AppleDouble
                .LSOverride

                # Windows
                Thumbs.db
                Desktop.ini

                # Linux / Unix desktop metadata
                .directory
                *~
                """;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private record ProjectManifestSummary(String name, String version, String language, String stdlib) {
    }

    private String sanitizeProjectName(String rawName) {
        final String normalized = Objects.requireNonNull(rawName, "rawName")
                .trim()
                .toLowerCase(Locale.ROOT)
                .replaceAll("[^a-z0-9]+", "-")
                .replaceAll("^-+", "")
                .replaceAll("-+$", "");
        return normalized;
    }
}
