use alcomd3_mcp_protocol::{
    ClientIdentity, EndpointMetadata, IPC_PROTOCOL_VERSION, IpcRequest, IpcResponse,
    endpoint_file_path,
};
use anyhow::{Context, Result, bail};
use rmcp::{
    Json, Peer, RoleServer, ServerHandler, ServiceExt,
    handler::server::wrapper::Parameters,
    model::{Implementation, JsonObject, ServerCapabilities, ServerInfo},
    schemars, tool, tool_handler, tool_router,
    transport::stdio,
};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Command as ProcessCommand, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use uuid::Uuid;

const GUI_EXECUTABLE_ENV: &str = "ALCOMD3_GUI_EXECUTABLE";
const GUI_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
const GUI_STARTUP_POLL_INTERVAL: Duration = Duration::from_millis(250);

#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
struct ProjectDetailsArgs {
    project_path: String,
}

#[derive(Debug, Clone, Deserialize, Serialize, schemars::JsonSchema)]
struct SearchPackagesArgs {
    query: Option<String>,
    limit: Option<u32>,
}

#[derive(Clone)]
struct Alcomd3Mcp {
    client: Arc<Mutex<ClientIdentity>>,
}

impl Alcomd3Mcp {
    fn new() -> Self {
        Self {
            client: Arc::new(Mutex::new(ClientIdentity {
                session_id: Uuid::new_v4(),
                name: "MCP client".to_string(),
                version: None,
            })),
        }
    }

    fn update_client(&self, peer: &Peer<RoleServer>) {
        let Some(info) = peer.peer_info() else {
            return;
        };
        let mut client = self.client.lock().unwrap();
        client.name = info.client_info.name.clone();
        client.version = Some(info.client_info.version.clone());
    }

    async fn invoke<T: Serialize>(
        &self,
        method: &str,
        params: T,
        peer: Peer<RoleServer>,
    ) -> Json<JsonObject> {
        self.update_client(&peer);
        let client = self.client.lock().unwrap().clone();
        format_invoke_result(
            invoke_gui(
                method,
                serde_json::to_value(params).unwrap_or(Value::Null),
                &client,
            )
            .await,
        )
    }
}

#[tool_router]
impl Alcomd3Mcp {
    #[tool(
        description = "List Unity projects registered in ALCOMD3",
        annotations(
            read_only_hint = true,
            destructive_hint = false,
            idempotent_hint = true,
            open_world_hint = false
        )
    )]
    async fn alcomd3_list_projects(&self, peer: Peer<RoleServer>) -> Json<JsonObject> {
        self.invoke("list_projects", json!({}), peer).await
    }

    #[tool(
        description = "Get details for a project registered in ALCOMD3. project_path must match an ALCOMD3 registered project path.",
        annotations(
            read_only_hint = true,
            destructive_hint = false,
            idempotent_hint = true,
            open_world_hint = false
        )
    )]
    async fn alcomd3_get_project_details(
        &self,
        Parameters(_args): Parameters<ProjectDetailsArgs>,
        peer: Peer<RoleServer>,
    ) -> Json<JsonObject> {
        self.invoke("get_project_details", _args, peer).await
    }

    #[tool(
        description = "List VPM repositories configured in ALCOMD3",
        annotations(
            read_only_hint = true,
            destructive_hint = false,
            idempotent_hint = true,
            open_world_hint = false
        )
    )]
    async fn alcomd3_list_repositories(&self, peer: Peer<RoleServer>) -> Json<JsonObject> {
        self.invoke("list_repositories", json!({}), peer).await
    }

    #[tool(
        description = "Search packages visible to ALCOMD3 without forcing a repository refresh. limit defaults to 30 and is capped at 100.",
        annotations(
            read_only_hint = true,
            destructive_hint = false,
            idempotent_hint = true,
            open_world_hint = false
        )
    )]
    async fn alcomd3_search_packages(
        &self,
        Parameters(_args): Parameters<SearchPackagesArgs>,
        peer: Peer<RoleServer>,
    ) -> Json<JsonObject> {
        self.invoke("search_packages", _args, peer).await
    }
}

#[tool_handler]
impl ServerHandler for Alcomd3Mcp {
    fn get_info(&self) -> ServerInfo {
        ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
            .with_server_info(Implementation::new(
                "alcomd3-mcp",
                env!("CARGO_PKG_VERSION"),
            ))
            .with_instructions(
                "Use ALCOMD3 read-only tools through the local GUI IPC endpoint. Tool calls may start the ALCOMD3 GUI if it is not running.",
            )
    }
}

async fn invoke_gui(method: &str, params: Value, client: &ClientIdentity) -> Result<Value> {
    match invoke_gui_once(method, params.clone(), client).await {
        Ok(value) => Ok(value),
        Err(first_error) if should_try_start_gui(&first_error) => {
            start_alcom_gui().with_context(|| {
                format!("starting ALCOMD3 GUI after MCP IPC became unavailable: {first_error:#}")
            })?;
            wait_for_gui_and_invoke(method, params, client, first_error).await
        }
        Err(error) => Err(error),
    }
}

async fn invoke_gui_once(method: &str, params: Value, client: &ClientIdentity) -> Result<Value> {
    let metadata = read_endpoint().await?;
    if metadata.protocol_version != IPC_PROTOCOL_VERSION {
        bail!(
            "ALCOMD3 IPC protocol mismatch: bridge={}, GUI={}",
            IPC_PROTOCOL_VERSION,
            metadata.protocol_version
        );
    }

    let request_id = Uuid::new_v4();
    let request = IpcRequest {
        protocol_version: IPC_PROTOCOL_VERSION,
        token: metadata.token.clone(),
        request_id,
        client: client.clone(),
        method: method.to_string(),
        params,
    };

    let response = invoke_tcp(&metadata, &request).await?;
    if response.request_id != request_id {
        bail!("ALCOMD3 returned a response for a different request");
    }

    Ok(response_to_tool_value(response))
}

async fn wait_for_gui_and_invoke(
    method: &str,
    params: Value,
    client: &ClientIdentity,
    first_error: anyhow::Error,
) -> Result<Value> {
    let started = Instant::now();
    let mut last_error = first_error;

    while started.elapsed() < GUI_STARTUP_TIMEOUT {
        tokio::time::sleep(GUI_STARTUP_POLL_INTERVAL).await;
        match invoke_gui_once(method, params.clone(), client).await {
            Ok(value) => return Ok(value),
            Err(error) if should_try_start_gui(&error) => last_error = error,
            Err(error) => {
                return Err(error)
                    .context("waiting for ALCOMD3 GUI MCP endpoint after starting GUI");
            }
        }
    }

    Err(last_error).context("waiting for ALCOMD3 GUI MCP endpoint after starting GUI")
}

async fn read_endpoint() -> Result<EndpointMetadata> {
    let path = endpoint_file_path();
    let bytes = tokio::fs::read(&path).await.with_context(|| {
        format!(
            "reading ALCOMD3 MCP endpoint metadata at {}",
            path.display()
        )
    })?;
    serde_json::from_slice(&bytes).context("parsing ALCOMD3 MCP endpoint metadata")
}

async fn invoke_tcp(metadata: &EndpointMetadata, request: &IpcRequest) -> Result<IpcResponse> {
    let stream = TcpStream::connect((metadata.host.as_str(), metadata.port))
        .await
        .with_context(|| {
            format!(
                "connecting to ALCOMD3 MCP IPC endpoint {}:{}",
                metadata.host, metadata.port
            )
        })?;

    write_request_and_read_response(stream, request).await
}

async fn write_request_and_read_response(
    stream: TcpStream,
    request: &IpcRequest,
) -> Result<IpcResponse> {
    let (read_half, mut write_half) = tokio::io::split(stream);
    let line = serde_json::to_vec(request)?;
    write_half.write_all(&line).await?;
    write_half.write_all(b"\n").await?;
    write_half.flush().await?;

    let mut reader = BufReader::new(read_half);
    let mut response = String::new();
    if reader.read_line(&mut response).await? == 0 {
        return Err(io::Error::new(
            io::ErrorKind::UnexpectedEof,
            "ALCOMD3 MCP IPC endpoint closed without a response",
        )
        .into());
    }
    serde_json::from_str(&response).context("parsing ALCOMD3 MCP IPC response")
}

fn response_to_tool_value(response: IpcResponse) -> Value {
    if response.ok {
        match response.result {
            Some(Value::Object(mut object)) => {
                object.insert("ok".to_string(), Value::Bool(true));
                Value::Object(object)
            }
            Some(value) => json!({
                "ok": true,
                "result": value,
            }),
            None => json!({ "ok": true }),
        }
    } else {
        json!({
            "ok": false,
            "error": response.error,
        })
    }
}

fn format_invoke_result(result: Result<Value>) -> Json<JsonObject> {
    match result {
        Ok(value) => Json(value_as_object(value)),
        Err(error) => Json(value_as_object(json!({
            "ok": false,
            "error": {
                "code": "alcomd3_unavailable",
                "message": format!(
                    "ALCOMD3 is not running or the MCP IPC endpoint is unavailable: {error}"
                ),
            }
        }))),
    }
}

fn value_as_object(value: Value) -> JsonObject {
    match value {
        Value::Object(object) => object,
        value => {
            let mut object = JsonObject::new();
            object.insert("result".to_string(), value);
            object
        }
    }
}

fn should_try_start_gui(error: &anyhow::Error) -> bool {
    error.chain().any(|cause| {
        cause.downcast_ref::<io::Error>().is_some_and(|io_error| {
            matches!(
                io_error.kind(),
                io::ErrorKind::NotFound
                    | io::ErrorKind::ConnectionRefused
                    | io::ErrorKind::ConnectionReset
                    | io::ErrorKind::ConnectionAborted
                    | io::ErrorKind::TimedOut
                    | io::ErrorKind::BrokenPipe
                    | io::ErrorKind::UnexpectedEof
            )
        })
    })
}

fn start_alcom_gui() -> Result<PathBuf> {
    let path = gui_executable_path().context("locating ALCOMD3 GUI executable")?;
    let mut child = ProcessCommand::new(&path)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .spawn()
        .with_context(|| format!("starting ALCOMD3 GUI at {}", path.display()))?;

    std::thread::spawn(move || {
        let _ = child.wait();
    });

    Ok(path)
}

fn gui_executable_path() -> Option<PathBuf> {
    if let Some(path) = std::env::var_os(GUI_EXECUTABLE_ENV) {
        return Some(PathBuf::from(path));
    }

    let current = std::env::current_exe().ok()?;
    gui_executable_candidates(&current)
        .into_iter()
        .find(|path| path.is_file())
        .or_else(|| gui_executable_candidates(&current).into_iter().next())
}

fn gui_executable_candidates(current_exe: &Path) -> Vec<PathBuf> {
    let directory = current_exe.parent().unwrap_or_else(|| Path::new("."));

    #[cfg(windows)]
    {
        vec![directory.join("ALCOM.exe")]
    }
    #[cfg(not(windows))]
    {
        vec![directory.join("ALCOM"), directory.join("alcom")]
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let service = Alcomd3Mcp::new().serve(stdio()).await?;
    service.waiting().await?;
    Ok(())
}
