use adapters::latest_github_release;
use anyhow::Context as _;
use anyhow::bail;
use dap::StartDebuggingRequestArguments;
use dap::StartDebuggingRequestArgumentsRequest;
use dap::adapters::{DebugTaskDefinition, TcpArguments};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use util::ResultExt;

use crate::*;

#[derive(Default)]
pub(crate) struct PhpDebugAdapter {
    checked: OnceLock<()>,
}

impl PhpDebugAdapter {
    const ADAPTER_NAME: &'static str = "PHP";
    const ADAPTER_PACKAGE_NAME: &'static str = "vscode-php-debug";
    const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";

    async fn fetch_latest_adapter_version(
        &self,
        delegate: &Arc<dyn DapDelegate>,
    ) -> Result<AdapterVersion> {
        let release = latest_github_release(
            &format!("{}/{}", "xdebug", Self::ADAPTER_PACKAGE_NAME),
            true,
            false,
            delegate.http_client(),
        )
        .await?;

        let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));

        Ok(AdapterVersion {
            tag_name: release.tag_name,
            url: release
                .assets
                .iter()
                .find(|asset| asset.name == asset_name)
                .with_context(|| format!("no asset found matching {asset_name:?}"))?
                .browser_download_url
                .clone(),
        })
    }

    async fn get_installed_binary(
        &self,
        delegate: &Arc<dyn DapDelegate>,
        task_definition: &DebugTaskDefinition,
        user_installed_path: Option<PathBuf>,
        user_args: Option<Vec<String>>,
        _: &mut AsyncApp,
    ) -> Result<DebugAdapterBinary> {
        let adapter_path = if let Some(user_installed_path) = user_installed_path {
            user_installed_path
        } else {
            let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());

            let file_name_prefix = format!("{}_", self.name());

            util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
                file_name.starts_with(&file_name_prefix)
            })
            .await
            .context("Couldn't find PHP dap directory")?
        };

        let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
        let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;

        let mut configuration = task_definition.config.clone();
        if let Some(obj) = configuration.as_object_mut() {
            obj.entry("cwd")
                .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
        }

        let arguments = if let Some(mut args) = user_args {
            args.insert(
                0,
                adapter_path
                    .join(Self::ADAPTER_PATH)
                    .to_string_lossy()
                    .to_string(),
            );
            args
        } else {
            vec![
                adapter_path
                    .join(Self::ADAPTER_PATH)
                    .to_string_lossy()
                    .to_string(),
                format!("--server={}", port),
            ]
        };

        Ok(DebugAdapterBinary {
            command: Some(
                delegate
                    .node_runtime()
                    .binary_path()
                    .await?
                    .to_string_lossy()
                    .into_owned(),
            ),
            arguments,
            connection: Some(TcpArguments {
                port,
                host,
                timeout,
            }),
            cwd: Some(delegate.worktree_root_path().to_path_buf()),
            envs: HashMap::default(),
            request_args: StartDebuggingRequestArguments {
                configuration,
                request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)
                    .await?,
            },
        })
    }
}

#[async_trait(?Send)]
impl DebugAdapter for PhpDebugAdapter {
    fn dap_schema(&self) -> serde_json::Value {
        json!({
            "properties": {
                "request": {
                    "type": "string",
                    "enum": ["launch"],
                    "description": "The request type for the PHP debug adapter, always \"launch\"",
                    "default": "launch"
                },
                "hostname": {
                    "type": "string",
                    "description": "The address to bind to when listening for Xdebug (default: all IPv6 connections if available, else all IPv4 connections) or Unix Domain socket (prefix with unix://) or Windows Pipe (\\\\?\\pipe\\name) - cannot be combined with port"
                },
                "port": {
                    "type": "integer",
                    "description": "The port on which to listen for Xdebug (default: 9003). If port is set to 0 a random port is chosen by the system and a placeholder ${port} is replaced with the chosen port in env and runtimeArgs.",
                    "default": 9003
                },
                "program": {
                    "type": "string",
                    "description": "The PHP script to debug (typically a path to a file)",
                    "default": "${file}"
                },
                "cwd": {
                    "type": "string",
                    "description": "Working directory for the debugged program"
                },
                "args": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    },
                    "description": "Command line arguments to pass to the program"
                },
                "env": {
                    "type": "object",
                    "description": "Environment variables to pass to the program",
                    "additionalProperties": {
                        "type": "string"
                    }
                },
                "stopOnEntry": {
                    "type": "boolean",
                    "description": "Whether to break at the beginning of the script",
                    "default": false
                },
                "pathMappings": {
                    "type": "object",
                    "description": "A mapping of server paths to local paths.",
                },
                "log": {
                    "type": "boolean",
                    "description": "Whether to log all communication between editor and the adapter to the debug console",
                    "default": false
                },
                "ignore": {
                    "type": "array",
                    "description": "An array of glob patterns that errors should be ignored from (for example **/vendor/**/*.php)",
                    "items": {
                        "type": "string"
                    }
                },
                "ignoreExceptions": {
                    "type": "array",
                    "description": "An array of exception class names that should be ignored (for example BaseException, \\NS1\\Exception, \\*\\Exception or \\**\\Exception*)",
                    "items": {
                        "type": "string"
                    }
                },
                "skipFiles": {
                    "type": "array",
                    "description": "An array of glob patterns to skip when debugging. Star patterns and negations are allowed.",
                    "items": {
                        "type": "string"
                    }
                },
                "skipEntryPaths": {
                    "type": "array",
                    "description": "An array of glob patterns to immediately detach from and ignore for debugging if the entry script matches",
                    "items": {
                        "type": "string"
                    }
                },
                "maxConnections": {
                    "type": "integer",
                    "description": "Accept only this number of parallel debugging sessions. Additional connections will be dropped.",
                    "default": 1
                },
                "proxy": {
                    "type": "object",
                    "description": "DBGp Proxy settings",
                    "properties": {
                        "enable": {
                            "type": "boolean",
                            "description": "To enable proxy registration",
                            "default": false
                        },
                        "host": {
                            "type": "string",
                            "description": "The address of the proxy. Supports host name, IP address, or Unix domain socket.",
                            "default": "127.0.0.1"
                        },
                        "port": {
                            "type": "integer",
                            "description": "The port where the adapter will register with the proxy",
                            "default": 9001
                        },
                        "key": {
                            "type": "string",
                            "description": "A unique key that allows the proxy to match requests to your editor",
                            "default": "vsc"
                        },
                        "timeout": {
                            "type": "integer",
                            "description": "The number of milliseconds to wait before giving up on the connection to proxy",
                            "default": 3000
                        },
                        "allowMultipleSessions": {
                            "type": "boolean",
                            "description": "If the proxy should forward multiple sessions/connections at the same time or not",
                            "default": true
                        }
                    }
                },
                "xdebugSettings": {
                    "type": "object",
                    "description": "Allows you to override Xdebug's remote debugging settings to fine tune Xdebug to your needs",
                    "properties": {
                        "max_children": {
                            "type": "integer",
                            "description": "Max number of array or object children to initially retrieve"
                        },
                        "max_data": {
                            "type": "integer",
                            "description": "Max amount of variable data to initially retrieve"
                        },
                        "max_depth": {
                            "type": "integer",
                            "description": "Maximum depth that the debugger engine may return when sending arrays, hashes or object structures to the IDE"
                        },
                        "show_hidden": {
                            "type": "integer",
                            "description": "Whether to show detailed internal information on properties (e.g. private members of classes). Zero means hidden members are not shown.",
                            "enum": [0, 1]
                        },
                        "breakpoint_include_return_value": {
                            "type": "boolean",
                            "description": "Determines whether to enable an additional \"return from function\" debugging step, allowing inspection of the return value when a function call returns"
                        }
                    }
                },
                "xdebugCloudToken": {
                    "type": "string",
                    "description": "Instead of listening locally, open a connection and register with Xdebug Cloud and accept debugging sessions on that connection"
                },
                "stream": {
                    "type": "object",
                    "description": "Allows to influence DBGp streams. Xdebug only supports stdout",
                    "properties": {
                        "stdout": {
                            "type": "integer",
                            "description": "Redirect stdout stream: 0 (disable), 1 (copy), 2 (redirect)",
                            "enum": [0, 1, 2],
                            "default": 0
                        }
                    }
                }
            },
            "required": ["request", "program"]
        })
    }

    fn name(&self) -> DebugAdapterName {
        DebugAdapterName(Self::ADAPTER_NAME.into())
    }

    fn adapter_language_name(&self) -> Option<LanguageName> {
        Some(SharedString::new_static("PHP").into())
    }

    async fn request_kind(
        &self,
        _: &serde_json::Value,
    ) -> Result<StartDebuggingRequestArgumentsRequest> {
        Ok(StartDebuggingRequestArgumentsRequest::Launch)
    }

    async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
        let obj = match &zed_scenario.request {
            dap::DebugRequest::Attach(_) => {
                bail!("Php adapter doesn't support attaching")
            }
            dap::DebugRequest::Launch(launch_config) => json!({
                "program": launch_config.program,
                "cwd": launch_config.cwd,
                "args": launch_config.args,
                "env": launch_config.env_json(),
                "stopOnEntry": zed_scenario.stop_on_entry.unwrap_or_default(),
            }),
        };

        Ok(DebugScenario {
            adapter: zed_scenario.adapter,
            label: zed_scenario.label,
            build: None,
            config: obj,
            tcp_connection: None,
        })
    }

    async fn get_binary(
        &self,
        delegate: &Arc<dyn DapDelegate>,
        task_definition: &DebugTaskDefinition,
        user_installed_path: Option<PathBuf>,
        user_args: Option<Vec<String>>,
        cx: &mut AsyncApp,
    ) -> Result<DebugAdapterBinary> {
        if self.checked.set(()).is_ok() {
            delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
            if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
                adapters::download_adapter_from_github(
                    self.name(),
                    version,
                    adapters::DownloadedFileType::Vsix,
                    delegate.as_ref(),
                )
                .await?;
            }
        }

        self.get_installed_binary(
            delegate,
            &task_definition,
            user_installed_path,
            user_args,
            cx,
        )
        .await
    }
}
