use crate::config::Settings;
use std::borrow::Cow;
use std::fmt::Display;

use indoc::formatdoc;

use crate::shell::{ActivateOptions, Shell};

#[derive(Default)]
pub struct Pwsh {}

impl Shell for Pwsh {
    fn activate(&self, opts: ActivateOptions) -> String {
        let exe = opts.exe;
        let flags = opts.flags;
        let exe = exe.to_string_lossy();
        let mut out = String::new();

        out.push_str(&formatdoc! {r#"
            $env:MISE_SHELL = 'pwsh'
            $env:__MISE_ORIG_PATH = $env:PATH

            function mise {{
                # Read line directly from input to workaround powershell input parsing for functions
                $code = [System.Management.Automation.Language.Parser]::ParseInput($MyInvocation.Statement.Substring($MyInvocation.OffsetInLine - 1), [ref]$null, [ref]$null)
                $myLine = $code.Find({{ $args[0].CommandElements }}, $true).CommandElements | ForEach-Object {{ $_.ToString() }} | Join-String -Separator ' '
                $command, [array]$arguments = Invoke-Expression ('Write-Output -- ' + $myLine)
                
                if ($null -eq $arguments) {{ 
                    & {exe}
                    return
                }} 

                $command = $arguments[0]
                $arguments = $arguments[1..$arguments.Length]

                if ($arguments -contains '--help') {{
                    return & {exe} $command $arguments 
                }}

                switch ($command) {{
                    {{ $_ -in 'deactivate', 'shell', 'sh' }} {{
                        if ($arguments -contains '-h' -or $arguments -contains '--help') {{
                            & {exe} $command $arguments
                        }}
                        else {{
                            & {exe} $command $arguments | Out-String | Invoke-Expression -ErrorAction SilentlyContinue
                        }}
                    }}
                    default {{
                        & {exe} $command $arguments
                        $status = $LASTEXITCODE
                        if ($(Test-Path -Path Function:\_mise_hook)){{
                            _mise_hook
                        }}
                        # Pass down exit code from mise after _mise_hook
                        pwsh -NoProfile -Command exit $status 
                    }}
                }}
            }}
            "#});

        if !opts.no_hook_env {
            out.push_str(&formatdoc! {r#"

            function Global:_mise_hook {{
                if ($env:MISE_SHELL -eq "pwsh"){{
                    & {exe} hook-env{flags} $args -s pwsh | Out-String | Invoke-Expression -ErrorAction SilentlyContinue
                }}
            }}

            function __enable_mise_chpwd{{
                if (-not $__mise_pwsh_chpwd){{
                    $Global:__mise_pwsh_chpwd= $true
                    $_mise_chpwd_hook = [EventHandler[System.Management.Automation.LocationChangedEventArgs]] {{
                        param([object] $source, [System.Management.Automation.LocationChangedEventArgs] $eventArgs)
                        end {{
                            _mise_hook
                        }}
                    }};
                    $__mise_pwsh_previous_chpwd_function=$ExecutionContext.SessionState.InvokeCommand.LocationChangedAction;

                    if ($__mise_original_pwsh_chpwd_function) {{
                        $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = [Delegate]::Combine($__mise_pwsh_previous_chpwd_function, $_mise_chpwd_hook)
                    }}
                    else {{
                        $ExecutionContext.SessionState.InvokeCommand.LocationChangedAction = $_mise_chpwd_hook
                    }}
                }}
            }}
            __enable_mise_chpwd
            Remove-Item -ErrorAction SilentlyContinue -Path Function:/__enable_mise_chpwd

            function __enable_mise_prompt {{
                if (-not $__mise_pwsh_previous_prompt_function){{
                    $Global:__mise_pwsh_previous_prompt_function=$function:prompt
                    function global:prompt {{
                        if (Test-Path -Path Function:\_mise_hook){{
                            _mise_hook
                        }}
                        & $__mise_pwsh_previous_prompt_function
                    }}
                }}
            }}
            __enable_mise_prompt
            Remove-Item -ErrorAction SilentlyContinue -Path Function:/__enable_mise_prompt

            _mise_hook
            "#});
        }
        if Settings::get().not_found_auto_install {
            out.push_str(&formatdoc! {r#"
            if (-not $__mise_pwsh_command_not_found){{
                $Global:__mise_pwsh_command_not_found= $true
                function __enable_mise_command_not_found {{
                    $_mise_pwsh_cmd_not_found_hook = [EventHandler[System.Management.Automation.CommandLookupEventArgs]] {{
                        param([object] $Name, [System.Management.Automation.CommandLookupEventArgs] $eventArgs)
                        end {{
                            if ([Microsoft.PowerShell.PSConsoleReadLine]::GetHistoryItems()[-1].CommandLine -match ([regex]::Escape($Name))) {{
                                if (& {exe} hook-not-found -s pwsh -- $Name){{
                                    _mise_hook
                                    if (Get-Command $Name -ErrorAction SilentlyContinue){{
                                        $EventArgs.Command = Get-Command $Name
                                        $EventArgs.StopSearch = $true
                                    }}
                                }}
                            }}
                        }}
                    }}
                    $current_command_not_found_function = $ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction
                    if ($current_command_not_found_function) {{
                        $ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = [Delegate]::Combine($current_command_not_found_function, $_mise_pwsh_cmd_not_found_hook)
                    }}
                    else {{
                        $ExecutionContext.SessionState.InvokeCommand.CommandNotFoundAction = $_mise_pwsh_cmd_not_found_hook
                    }}
                }}
                __enable_mise_command_not_found
                Remove-Item -ErrorAction SilentlyContinue -Path Function:/__enable_mise_command_not_found
            }}
            "#});
        }
        out
    }

    fn deactivate(&self) -> String {
        formatdoc! {r#"
        Remove-Item -ErrorAction SilentlyContinue function:mise
        Remove-Item -ErrorAction SilentlyContinue -Path Env:/MISE_SHELL
        Remove-Item -ErrorAction SilentlyContinue -Path Env:/__MISE_WATCH
        Remove-Item -ErrorAction SilentlyContinue -Path Env:/__MISE_DIFF
        "#}
    }

    fn set_env(&self, k: &str, v: &str) -> String {
        let k = powershell_escape(k.into());
        let v = powershell_escape(v.into());
        format!("$Env:{k}='{v}'\n")
    }

    fn prepend_env(&self, k: &str, v: &str) -> String {
        let k = powershell_escape(k.into());
        let v = powershell_escape(v.into());
        format!("$Env:{k}='{v}'+[IO.Path]::PathSeparator+$env:{k}\n")
    }

    fn unset_env(&self, k: &str) -> String {
        let k = powershell_escape(k.into());
        format!("Remove-Item -ErrorAction SilentlyContinue -Path Env:/{k}\n")
    }
}

impl Display for Pwsh {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "pwsh")
    }
}

fn powershell_escape(s: Cow<str>) -> Cow<str> {
    let needs_escape = s.is_empty();

    if !needs_escape {
        return s;
    }

    let mut es = String::with_capacity(s.len());
    let mut chars = s.chars().peekable();
    loop {
        match chars.next() {
            Some('\t') => {
                es.push_str("`t");
            }
            Some('\n') => {
                es.push_str("`n");
            }
            Some('\r') => {
                es.push_str("`r");
            }
            Some('\'') => {
                es.push_str("`'");
            }
            Some('`') => {
                es.push_str("``");
            }
            Some(c) => {
                es.push(c);
            }
            None => {
                break;
            }
        }
    }
    es.into()
}

#[cfg(test)]
mod tests {
    use insta::assert_snapshot;
    use std::path::Path;
    use test_log::test;

    use crate::test::replace_path;

    use super::*;

    #[test]
    fn test_activate() {
        let pwsh = Pwsh::default();
        let exe = Path::new("/some/dir/mise");
        let opts = ActivateOptions {
            exe: exe.to_path_buf(),
            flags: " --status".into(),
            no_hook_env: false,
        };
        assert_snapshot!(pwsh.activate(opts));
    }

    #[test]
    fn test_set_env() {
        assert_snapshot!(Pwsh::default().set_env("FOO", "1"));
    }

    #[test]
    fn test_prepend_env() {
        let pwsh = Pwsh::default();
        assert_snapshot!(replace_path(&pwsh.prepend_env("PATH", "/some/dir:/2/dir")));
    }

    #[test]
    fn test_unset_env() {
        assert_snapshot!(Pwsh::default().unset_env("FOO"));
    }

    #[test]
    fn test_deactivate() {
        let deactivate = Pwsh::default().deactivate();
        assert_snapshot!(replace_path(&deactivate));
    }
}
