use anyhow::{Context, Result};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;

#[cfg(windows)]
use crate::cli::{HotkeyAction, HotkeyCommand, HotkeyStartArgs};
#[cfg(windows)]
use std::io;

struct HookSpec {
    hook: &'static str,
    marker_start: &'static str,
    marker_end: &'static str,
    legacy_marker_start: &'static str,
    legacy_marker_end: &'static str,
    legacy_hook: Option<&'static str>,
}

const LEGACY_MARKER_START: &str = "# >>> sift shell integration >>>";
const LEGACY_MARKER_END: &str = "# <<< sift shell integration <<<";
const POWERSHELL_MARKER_START: &str = "# >>> sivtr shell integration >>>";
const POWERSHELL_MARKER_END: &str = "# <<< sivtr shell integration <<<";
const LEGACY_POWERSHELL_HOOK: &str = r#"# sift shell integration
$env:SIFT_SESSION_LOG = Join-Path $env:APPDATA "sift\session_$PID.log"
$Global:_sift_orig_prompt = $function:prompt
function Global:prompt { try { sift flush *>$null } catch {}; & $Global:_sift_orig_prompt }
"#;
const POWERSHELL_HOOK: &str = r#"# >>> sivtr shell integration >>>
$env:SIVTR_SESSION_LOG = Join-Path $env:APPDATA "sivtr\session_$PID.log"
if (-not $Global:_sivtr_prompt_wrapped) {
    $Global:_sivtr_orig_prompt = $function:prompt
    function Global:prompt {
        $rendered = if ($Global:_sivtr_orig_prompt) {
            & $Global:_sivtr_orig_prompt
        } else {
            "PS $($executionContext.SessionState.Path.CurrentLocation)> "
        }
        try {
            $last = Get-History -Count 1 -ErrorAction SilentlyContinue
            if ($last) {
                $env:SIVTR_LAST_COMMAND = [string]$last.CommandLine
                $env:SIVTR_LAST_COMMAND_ID = [string]$last.Id
            }
            $env:SIVTR_LAST_PROMPT = [string]$rendered
            sivtr flush
        } catch {}
        $rendered
    }
    $Global:_sivtr_prompt_wrapped = $true
}
# <<< sivtr shell integration <<<
"#;

const BASH_MARKER_START: &str = "# >>> sivtr shell integration >>>";
const BASH_MARKER_END: &str = "# <<< sivtr shell integration <<<";
const BASH_HOOK: &str = r#"# >>> sivtr shell integration >>>
if [[ -n "${APPDATA:-}" ]]; then
  export SIVTR_SESSION_LOG="${APPDATA}\\sivtr\\session_$$.log"
else
  export SIVTR_SESSION_LOG="${XDG_STATE_HOME:-$HOME/.local/state}/sivtr/session_$$.log"
fi
__sivtr_precmd() {
  local hist_entry
  hist_entry="$(HISTTIMEFORMAT= history 1)"
  if [[ $hist_entry =~ ^[[:space:]]*([0-9]+)[[:space:]]+(.*)$ ]]; then
    export SIVTR_LAST_COMMAND_ID="${BASH_REMATCH[1]}"
    export SIVTR_LAST_COMMAND="${BASH_REMATCH[2]}"
  fi
  export SIVTR_LAST_PROMPT="${PS1@P}"
  sivtr flush >/dev/null 2>&1 || true
}
if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then
  if [[ " ${PROMPT_COMMAND[*]} " != *" __sivtr_precmd "* ]]; then
    PROMPT_COMMAND=(__sivtr_precmd "${PROMPT_COMMAND[@]}")
  fi
elif [[ -n "${PROMPT_COMMAND:-}" ]]; then
  case ";$PROMPT_COMMAND;" in
    *";__sivtr_precmd;"*) ;;
    *) PROMPT_COMMAND="__sivtr_precmd;$PROMPT_COMMAND" ;;
  esac
else
  PROMPT_COMMAND="__sivtr_precmd"
fi
# <<< sivtr shell integration <<<
"#;

const ZSH_MARKER_START: &str = "# >>> sivtr shell integration >>>";
const ZSH_MARKER_END: &str = "# <<< sivtr shell integration <<<";
const ZSH_HOOK: &str = r#"# >>> sivtr shell integration >>>
if [[ -n "${APPDATA:-}" ]]; then
  export SIVTR_SESSION_LOG="${APPDATA}\\sivtr\\session_$$.log"
else
  export SIVTR_SESSION_LOG="${XDG_STATE_HOME:-$HOME/.local/state}/sivtr/session_$$.log"
fi
_sivtr_precmd() {
  export SIVTR_LAST_COMMAND="$(fc -ln -1)"
  export SIVTR_LAST_COMMAND_ID="$HISTCMD"
  export SIVTR_LAST_PROMPT="$(print -P "$PROMPT")"
  sivtr flush >/dev/null 2>&1 || true
}
if (( ${precmd_functions[(I)_sivtr_precmd]} == 0 )); then
  precmd_functions=(_sivtr_precmd $precmd_functions)
fi
# <<< sivtr shell integration <<<
"#;

const NUSHELL_MARKER_START: &str = "# >>> sivtr shell integration >>>";
const NUSHELL_MARKER_END: &str = "# <<< sivtr shell integration <<<";
const NUSHELL_HOOK: &str = r#"# >>> sivtr shell integration >>>
$env.SIVTR_SESSION_LOG = (($env.APPDATA? | default $nu.default-config-dir) | path join 'sivtr' $"session_($nu.pid).log")
if (($env.SIVTR_PROMPT_WRAPPED? | default false) != true) {
    def _sivtr_precmd [] {
        let last = (history | last 1 | get 0?)
        if $last != null {
            $env.SIVTR_LAST_COMMAND = ($last.command? | default "")
            $env.SIVTR_LAST_COMMAND_ID = (($last.start_timestamp? | default (date now)) | into string)
        }
        $env.SIVTR_LAST_PROMPT = (do $env.PROMPT_COMMAND)
        try { ^sivtr flush } catch {}
    }
    $env.config.hooks.pre_prompt = ($env.config.hooks.pre_prompt? | default [] | append {|| _sivtr_precmd })
    $env.SIVTR_PROMPT_WRAPPED = true
}
# <<< sivtr shell integration <<<
"#;

const POWERSHELL_SPEC: HookSpec = HookSpec {
    hook: POWERSHELL_HOOK,
    marker_start: POWERSHELL_MARKER_START,
    marker_end: POWERSHELL_MARKER_END,
    legacy_marker_start: LEGACY_MARKER_START,
    legacy_marker_end: LEGACY_MARKER_END,
    legacy_hook: Some(LEGACY_POWERSHELL_HOOK),
};

const BASH_SPEC: HookSpec = HookSpec {
    hook: BASH_HOOK,
    marker_start: BASH_MARKER_START,
    marker_end: BASH_MARKER_END,
    legacy_marker_start: LEGACY_MARKER_START,
    legacy_marker_end: LEGACY_MARKER_END,
    legacy_hook: None,
};

const ZSH_SPEC: HookSpec = HookSpec {
    hook: ZSH_HOOK,
    marker_start: ZSH_MARKER_START,
    marker_end: ZSH_MARKER_END,
    legacy_marker_start: LEGACY_MARKER_START,
    legacy_marker_end: LEGACY_MARKER_END,
    legacy_hook: None,
};

const NUSHELL_SPEC: HookSpec = HookSpec {
    hook: NUSHELL_HOOK,
    marker_start: NUSHELL_MARKER_START,
    marker_end: NUSHELL_MARKER_END,
    legacy_marker_start: LEGACY_MARKER_START,
    legacy_marker_end: LEGACY_MARKER_END,
    legacy_hook: None,
};

enum InstallStatus {
    Installed,
    Updated,
    Unchanged,
}

/// Install shell hook into the shell's profile file (one-time setup).
pub fn execute(shell: &str) -> Result<()> {
    match shell.to_lowercase().as_str() {
        "powershell" | "pwsh" => install_powershell_hook(),
        "bash" => install_single_shell_hook(&bash_profile_path()?, &BASH_SPEC),
        "zsh" => install_single_shell_hook(&zsh_profile_path()?, &ZSH_SPEC),
        "nu" | "nushell" => install_single_shell_hook(&nushell_config_path()?, &NUSHELL_SPEC),
        _ => {
            eprintln!("sivtr: supported shells are powershell, bash, zsh, nushell");
            eprintln!("  usage: sivtr init <powershell|bash|zsh|nushell>");
            std::process::exit(1);
        }
    }?;

    maybe_start_hotkey()
}

fn install_powershell_hook() -> Result<()> {
    let mut installed = Vec::new();
    let mut updated = Vec::new();
    let mut failed = Vec::new();

    for cmd in &["pwsh", "powershell"] {
        if let Ok(path) = get_ps_profile(cmd) {
            match install_into_profile(Path::new(&path), &POWERSHELL_SPEC) {
                Ok(InstallStatus::Installed) => installed.push(path),
                Ok(InstallStatus::Updated) => updated.push(path),
                Ok(InstallStatus::Unchanged) => eprintln!("sivtr: already installed in {path}"),
                Err(err) => failed.push((path, err)),
            }
        }
    }

    print_install_summary(&installed, &updated);
    for (path, err) in failed {
        eprintln!("sivtr: failed to update {path}");
        eprintln!("  {err}");
    }
    Ok(())
}

fn install_single_shell_hook(profile_path: &Path, spec: &HookSpec) -> Result<()> {
    match install_into_profile(profile_path, spec)? {
        InstallStatus::Installed => {
            eprintln!("sivtr: installed into {}", profile_path.display());
            eprintln!("  restart your terminal to activate");
        }
        InstallStatus::Updated => {
            eprintln!("sivtr: updated {}", profile_path.display());
            eprintln!("  restart your terminal to activate");
        }
        InstallStatus::Unchanged => {
            eprintln!("sivtr: already installed in {}", profile_path.display());
            eprintln!("sivtr: no new installation needed (already set up)");
        }
    }
    Ok(())
}

fn print_install_summary(installed: &[String], updated: &[String]) {
    if installed.is_empty() && updated.is_empty() {
        eprintln!("sivtr: no new installation needed (already set up)");
        return;
    }

    for path in installed {
        eprintln!("sivtr: installed into {path}");
    }
    for path in updated {
        eprintln!("sivtr: updated {path}");
    }
    eprintln!("  restart your terminal to activate");
}

fn maybe_start_hotkey() -> Result<()> {
    #[cfg(windows)]
    {
        if !atty::is(atty::Stream::Stdin) {
            eprintln!("sivtr: start the Codex hotkey later with `sivtr hotkey start`");
            return Ok(());
        }

        if prompt_start_hotkey()? {
            crate::commands::hotkey::execute(HotkeyCommand {
                action: Some(HotkeyAction::Start(HotkeyStartArgs { chord: None })),
            })?;
        } else {
            eprintln!("sivtr: start the Codex hotkey later with `sivtr hotkey start`");
        }
    }

    Ok(())
}

#[cfg(windows)]
fn prompt_start_hotkey() -> Result<bool> {
    eprint!("sivtr: start the Windows Codex hotkey now? [Y/n] ");
    io::stderr().flush()?;

    let mut input = String::new();
    io::stdin().read_line(&mut input)?;
    Ok(parse_yes_no_default_yes(&input))
}

#[cfg(windows)]
fn parse_yes_no_default_yes(input: &str) -> bool {
    !matches!(input.trim().to_lowercase().as_str(), "n" | "no")
}

fn install_into_profile(profile_path: &Path, spec: &HookSpec) -> Result<InstallStatus> {
    if profile_path.exists() {
        let content = fs::read_to_string(profile_path)?;
        if let Some(updated) = update_existing_hook(&content, spec) {
            if updated == content {
                return Ok(InstallStatus::Unchanged);
            }
            fs::write(profile_path, updated)?;
            return Ok(InstallStatus::Updated);
        }
    }

    if let Some(parent) = profile_path.parent() {
        fs::create_dir_all(parent)?;
    }

    let mut file = fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(profile_path)?;
    writeln!(file, "\n{}", spec.hook)?;
    Ok(InstallStatus::Installed)
}

fn bash_profile_path() -> Result<PathBuf> {
    let home = dirs::home_dir().context("Failed to resolve home directory")?;
    Ok(home.join(".bashrc"))
}

fn zsh_profile_path() -> Result<PathBuf> {
    let home = dirs::home_dir().context("Failed to resolve home directory")?;
    Ok(home.join(".zshrc"))
}

fn nushell_config_path() -> Result<PathBuf> {
    if let Ok(path) = get_nu_config_path("nu") {
        return Ok(PathBuf::from(path));
    }

    let config_dir = dirs::config_dir().context("Failed to resolve config directory")?;
    Ok(config_dir.join("nushell").join("config.nu"))
}

fn get_ps_profile(cmd: &str) -> Result<String> {
    let output = Command::new(cmd)
        .args(["-NoProfile", "-Command", "Write-Output $PROFILE"])
        .output()
        .context("Failed to run shell")?;
    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if path.is_empty() {
        anyhow::bail!("empty profile path");
    }
    Ok(path)
}

fn get_nu_config_path(cmd: &str) -> Result<String> {
    let output = Command::new(cmd)
        .args(["-c", "print $nu.config-path"])
        .output()
        .context("Failed to run shell")?;
    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
    if path.is_empty() {
        anyhow::bail!("empty config path");
    }
    Ok(path)
}

fn update_existing_hook(content: &str, spec: &HookSpec) -> Option<String> {
    if content.contains(spec.hook) {
        return Some(content.to_string());
    }

    if let Some((start, end)) = find_marked_block(content, spec.marker_start, spec.marker_end) {
        let mut updated = String::with_capacity(content.len() - (end - start) + spec.hook.len());
        updated.push_str(&content[..start]);
        updated.push_str(spec.hook);
        updated.push_str(&content[end..]);
        return Some(updated);
    }

    if let Some((start, end)) =
        find_marked_block(content, spec.legacy_marker_start, spec.legacy_marker_end)
    {
        let mut updated = String::with_capacity(content.len() - (end - start) + spec.hook.len());
        updated.push_str(&content[..start]);
        updated.push_str(spec.hook);
        updated.push_str(&content[end..]);
        return Some(updated);
    }

    if let Some(legacy_hook) = spec.legacy_hook {
        if content.contains(legacy_hook) {
            return Some(content.replacen(legacy_hook, spec.hook, 1));
        }
    }

    None
}

fn find_marked_block(
    content: &str,
    start_marker: &str,
    end_marker: &str,
) -> Option<(usize, usize)> {
    let start = content.find(start_marker)?;
    let end_marker_offset = content[start..].find(end_marker)?;
    let end = start + end_marker_offset + end_marker.len();
    Some((start, end))
}

#[cfg(test)]
mod tests {
    use super::{
        update_existing_hook, BASH_HOOK, BASH_SPEC, LEGACY_POWERSHELL_HOOK, NUSHELL_HOOK,
        NUSHELL_SPEC, POWERSHELL_HOOK, POWERSHELL_SPEC, ZSH_HOOK, ZSH_SPEC,
    };

    #[cfg(windows)]
    use super::parse_yes_no_default_yes;

    #[test]
    fn upgrades_legacy_powershell_hook_in_place() {
        let profile = format!("before\n{LEGACY_POWERSHELL_HOOK}\nafter\n");
        let updated = update_existing_hook(&profile, &POWERSHELL_SPEC)
            .expect("legacy hook should be detected");

        assert!(updated.contains(POWERSHELL_HOOK));
        assert!(!updated.contains(LEGACY_POWERSHELL_HOOK));
        assert!(updated.contains("before"));
        assert!(updated.contains("after"));
    }

    #[test]
    fn keeps_current_powershell_hook_unchanged() {
        let profile = format!("before\n{POWERSHELL_HOOK}\nafter\n");
        let updated = update_existing_hook(&profile, &POWERSHELL_SPEC)
            .expect("current hook should be detected");

        assert_eq!(updated, profile);
    }

    #[test]
    fn current_powershell_hook_does_not_redirect_flush_output_handle() {
        assert!(POWERSHELL_HOOK.contains("sivtr flush"));
        assert!(!POWERSHELL_HOOK.contains("*>$null"));
    }

    #[test]
    fn replaces_existing_bash_block() {
        let profile = format!("before\n{BASH_HOOK}\nafter\n");
        let updated =
            update_existing_hook(&profile, &BASH_SPEC).expect("bash hook should be detected");

        assert_eq!(updated, profile);
    }

    #[test]
    fn replaces_existing_zsh_block() {
        let profile = format!("before\n{ZSH_HOOK}\nafter\n");
        let updated =
            update_existing_hook(&profile, &ZSH_SPEC).expect("zsh hook should be detected");

        assert_eq!(updated, profile);
    }

    #[test]
    fn replaces_existing_nushell_block() {
        let profile = format!("before\n{NUSHELL_HOOK}\nafter\n");
        let updated =
            update_existing_hook(&profile, &NUSHELL_SPEC).expect("nushell hook should be detected");

        assert_eq!(updated, profile);
    }

    #[cfg(windows)]
    #[test]
    fn hotkey_prompt_defaults_to_yes() {
        assert!(parse_yes_no_default_yes(""));
        assert!(parse_yes_no_default_yes("\n"));
        assert!(parse_yes_no_default_yes("y"));
        assert!(parse_yes_no_default_yes("YES"));
    }

    #[cfg(windows)]
    #[test]
    fn hotkey_prompt_accepts_no() {
        assert!(!parse_yes_no_default_yes("n"));
        assert!(!parse_yes_no_default_yes("No\n"));
    }
}
