package com.xebyte.headless;

import com.xebyte.core.*;
import ghidra.program.model.listing.Program;

import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Program and project management endpoints for headless mode.
 * Only passed to AnnotationScanner in GhidraMCPHeadlessServer,
 * so this category is absent from the GUI plugin schema.
 */
@McpToolGroup(value = "headless", description = "Headless server program management (no GUI required)")
public class HeadlessManagementService {

    private final HeadlessProgramProvider programProvider;
    private final GhidraServerManager serverManager;

    public HeadlessManagementService(HeadlessProgramProvider programProvider,
                                     GhidraServerManager serverManager) {
        this.programProvider = programProvider;
        this.serverManager = serverManager;
    }

    // ========================================================================
    // Program management
    // ========================================================================

    @McpTool(path = "/load_program", method = "POST", description = "Load a binary file into the headless server for analysis", category = "headless")
    public Response loadProgram(
            @Param(value = "file", source = ParamSource.BODY, description = "Absolute path to the binary file") String filePath) {
        if (filePath == null || filePath.isEmpty()) {
            return Response.err("file path required");
        }
        File file = new File(filePath);
        if (!file.exists()) {
            return Response.err("File not found: " + filePath);
        }
        Program program = programProvider.loadProgramFromFile(file);
        if (program != null) {
            return Response.text("{\"success\": true, \"program\": \"" + ServiceUtils.escapeJson(program.getName()) + "\"}");
        }
        return Response.err("Failed to load program from: " + filePath);
    }

    // ========================================================================
    // Project management
    // ========================================================================

    @McpTool(path = "/create_project", method = "POST", description = "Create a new Ghidra project", category = "headless")
    public Response createProject(
            @Param(value = "parentDir", source = ParamSource.BODY) String parentDir,
            @Param(value = "name", source = ParamSource.BODY) String name) {
        if (parentDir == null || parentDir.isEmpty()) return Response.err("parentDir required");
        if (name == null || name.isEmpty()) return Response.err("name required");
        try {
            boolean ok = programProvider.createProject(parentDir, name);
            if (ok) {
                return Response.text("{\"success\": true, \"name\": \"" + ServiceUtils.escapeJson(name)
                    + "\", \"path\": \"" + ServiceUtils.escapeJson(parentDir + "/" + name) + "\"}");
            }
            return Response.err("Failed to create project");
        } catch (Exception e) {
            return Response.err(e.getMessage());
        }
    }

    @McpTool(path = "/open_project", method = "POST", description = "Open an existing Ghidra project (.gpr file or directory)", category = "headless")
    public Response openProject(
            @Param(value = "path", source = ParamSource.BODY) String projectPath) {
        if (projectPath == null || projectPath.isEmpty()) {
            return Response.err("Project path required");
        }
        boolean success = programProvider.openProject(projectPath);
        if (success) {
            return Response.text("{\"success\": true, \"project\": \"" + ServiceUtils.escapeJson(programProvider.getProjectName()) + "\"}");
        }
        return Response.err("Failed to open project: " + projectPath);
    }

    @McpTool(path = "/close_project", method = "POST", description = "Close the currently open project", category = "headless")
    public Response closeProject() {
        if (!programProvider.hasProject()) {
            return Response.err("No project currently open");
        }
        String projectName = programProvider.getProjectName();
        programProvider.closeProject();
        return Response.text("{\"success\": true, \"closed\": \"" + ServiceUtils.escapeJson(projectName) + "\"}");
    }

    @McpTool(path = "/load_program_from_project", method = "POST", description = "Load a program from the open project. Returns structured diagnostics on failure (available paths, server-binding state) so the operator can tell server-side-checkout-but-not-shared from path-typo from server-unreachable. See discussion #119.", category = "headless")
    public Response loadProgramFromProject(
            @Param(value = "path", source = ParamSource.BODY, description = "Program path within the project") String programPath) {
        if (programPath == null || programPath.isEmpty()) {
            return Response.err("Program path required");
        }
        if (!programProvider.hasProject()) {
            return Response.err("No project open. Call /open_project first.");
        }

        HeadlessProgramProvider.ProgramLoadResult res =
            programProvider.loadProgramFromProjectDetailed(programPath);

        if (res.success) {
            Map<String, Object> ok = new LinkedHashMap<>();
            ok.put("success", true);
            ok.put("program", res.program.getName());
            ok.put("path", programPath);
            return Response.ok(ok);
        }

        // Structured failure — exposed so a Docker-headless user can tell
        // "wrong path" from "project not bound to server" from "server
        // unreachable" without needing to read Ghidra logs in the container.
        Map<String, Object> diagnostics = new LinkedHashMap<>();
        diagnostics.put("project_open", true);
        diagnostics.put("project_name", programProvider.getProjectName());
        HeadlessProgramProvider.ServerBindingInfo binding = programProvider.getProjectServerInfo();
        if (binding != null) {
            diagnostics.put("project_server_bound", binding.serverBound);
            if (binding.serverBound) {
                diagnostics.put("server", binding.serverInfo);
                diagnostics.put("server_repo", binding.repoName);
            }
        }
        if (res.availablePaths != null) {
            diagnostics.put("available_program_paths", res.availablePaths);
        }
        if (res.serverHint != null && !res.serverHint.isEmpty()) {
            diagnostics.put("suggestion", res.serverHint);
        }

        Map<String, Object> body = new LinkedHashMap<>();
        body.put("success", false);
        body.put("error", res.error);
        body.put("requested_path", programPath);
        body.put("diagnostics", diagnostics);
        return Response.ok(body);
    }

    @McpTool(path = "/get_project_info", description = "Get info about the currently open project, including server-binding state. A shared (server-bound) project is required for /server/version_control/checkout to deliver content the headless can open; if `project_server_bound` is false, the open project is local-only.", category = "headless")
    public Response getProjectInfo() {
        if (!programProvider.hasProject()) {
            return Response.text("{\"has_project\": false}");
        }
        List<HeadlessProgramProvider.ProjectFileInfo> files = programProvider.listProjectFiles();
        int programCount = (int) files.stream().filter(f -> "Program".equals(f.contentType)).count();

        Map<String, Object> info = new LinkedHashMap<>();
        info.put("has_project", true);
        info.put("project_name", programProvider.getProjectName());
        info.put("file_count", files.size());
        info.put("program_count", programCount);

        // Server-binding visibility (#119) — lets the operator confirm at
        // a glance whether checkout flows will actually deliver content
        // their /load_program_from_project can pick up.
        HeadlessProgramProvider.ServerBindingInfo binding = programProvider.getProjectServerInfo();
        if (binding != null) {
            info.put("project_server_bound", binding.serverBound);
            if (binding.serverBound) {
                info.put("server", binding.serverInfo);
                info.put("server_repo", binding.repoName);
            }
        }
        return Response.ok(info);
    }

    // ========================================================================
    // Server status
    // ========================================================================

    @McpTool(path = "/server/status", description = "Check headless server connection status", category = "headless")
    public Response serverStatus() {
        return Response.text(serverManager.getStatus());
    }
}
