//! Shell integration script content, stamped with the running
//! binary's version.
//!
//! The content is rendered to stdout by `rep init <shell>` and
//! consumed by the user's rc file via `eval "$(rep init zsh)"`.
//! There is no on-disk integration file — the script is regenerated
//! every shell session so a binary upgrade takes effect the next
//! time the user opens a terminal, with zero manual steps.
//!
//! This module also owns the first-run auto-wire path: when the user
//! installs via `cargo install reportal` (or any route that bypasses
//! the shell installer in `installer/`), the binary detects on the
//! very first invocation that `REPORTAL_LOADED` is unset and appends
//! the fenced `eval "$(rep init <shell>)"` block to the user's rc
//! file. The same fenced marker the shell installer uses guarantees
//! idempotence — subsequent runs (and subsequent shell-installer
//! runs) are no-ops.

use std::env;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::cli_args::InitShell;

/// Fenced marker opening the auto-managed integration block. This
/// string is byte-for-byte identical to the markers used by the shell
/// installer scripts in `installer/install.sh` and
/// `installer/install.ps1`, which makes both code paths interoperable:
/// whichever runs first claims the block, and the other becomes a
/// no-op.
const MARKER_START: &str = "# >>> reportal shell integration (do not edit) >>>";

/// Fenced marker closing the auto-managed integration block.
const MARKER_END: &str = "# <<< reportal shell integration <<<";

/// Returns the `PowerShell` integration script content, stamped with
/// the running binary's version.
///
/// Uses `global:` scope qualifiers on every function definition so
/// the script works identically whether the user pipes it into
/// `Invoke-Expression` from `$PROFILE`, dot-sources it into a module,
/// or feeds it through any other loading mechanism.
pub fn powershell_integration_content() -> String {
    format!(
        r#"# RePortal shell integration — v{}
# Generated by 'rep init powershell'.
$env:REPORTAL_LOADED = "1"
function global:rj {{ Set-Location (rep jump @args); rep color }}
function global:ro {{ rep open @args; rep color }}
function global:rjw {{ Set-Location (rep workspace jump @args); rep color }}
function global:row {{ rep workspace open @args; rep color }}
function global:rw {{ rep web @args }}
function global:rr {{ rep run @args }}
$script:_reportal_original_prompt = $function:global:prompt
function global:prompt {{ $p = & $script:_reportal_original_prompt; $t = rep color --mode prompt-hook 2>$null; if ($t) {{ $Host.UI.RawUI.WindowTitle = $t }}; $p }}
"#,
        env!("CARGO_PKG_VERSION"),
    )
}

/// Best-effort first-run hook that ensures the user's shell rc file
/// sources the `RePortal` integration.
///
/// Called from `main()` before subcommand dispatch (but skipped for
/// `rep init`, which must not produce stderr noise inside an
/// `eval "$(rep init zsh)"` invocation). When the integration is
/// already loaded in the current shell — detected via the
/// `REPORTAL_LOADED` env var the integration itself sets — this is
/// silent. Otherwise it resolves the user's login shell from `$SHELL`
/// (or `$PROFILE` on `PowerShell`), locates the appropriate rc file,
/// and, if the fenced marker is absent, appends an idempotent eval
/// block that mirrors exactly what the shell installer writes.
///
/// The function never returns an error to its caller: auto-setup is a
/// convenience for the `cargo install reportal` path and must never
/// break the user's actual command. Any failure (unsupported shell,
/// missing `$HOME`, unwritable rc file, unknown OS) is swallowed with
/// at most a single stderr warning.
pub fn ensure_shell_integration_installed() {
    if env::var_os("REPORTAL_LOADED").is_some() {
        return;
    }
    let Some(shell) = shell_from_env() else {
        return;
    };
    let Some(rc_file) = rc_file_for(shell) else {
        return;
    };
    match append_integration_if_missing(&rc_file, shell) {
        Ok(AutoSetupOutcome::AlreadyPresent) => {}
        Ok(AutoSetupOutcome::Appended) => {
            eprintln!(
                "RePortal: added shell integration to {} — run 'source {}' or open a new shell to activate.",
                rc_file.display(),
                rc_file.display(),
            );
        }
        Err(reason) => {
            eprintln!(
                "RePortal: note: could not auto-wire shell integration ({reason}); add `{}` to your rc file manually.",
                eval_line_for(shell),
            );
        }
    }
}

/// Result of a single auto-setup attempt. Used to decide whether the
/// caller should emit the stderr activation hint.
enum AutoSetupOutcome {
    /// Marker already present — no change made.
    AlreadyPresent,
    /// Fenced block appended to the rc file.
    Appended,
}

/// Parses `$SHELL`'s basename (or picks `PowerShell` on Windows when
/// `$SHELL` is absent) and returns the matching `InitShell` variant.
/// Returns `None` for fish, tcsh, csh, or anything else we don't
/// generate integration code for — those users can run
/// `rep init <shell>` themselves and will get a clear rejection.
fn shell_from_env() -> Option<InitShell> {
    if let Some(shell_path) = env::var_os("SHELL") {
        let basename = Path::new(&shell_path)
            .file_name()
            .and_then(|name| name.to_str())
            .map(str::to_ascii_lowercase);
        if let Some(name) = basename {
            return match name.as_str() {
                "zsh" => Some(InitShell::Zsh),
                "bash" => Some(InitShell::Bash),
                "pwsh" | "powershell" => Some(InitShell::Powershell),
                _ => None,
            };
        }
    }
    // Fallback: on Windows with no `$SHELL`, assume PowerShell. This
    // matches the installer's behavior and covers users who launched
    // from `pwsh`/`powershell.exe` directly.
    if cfg!(windows) {
        Some(InitShell::Powershell)
    } else {
        None
    }
}

/// Resolves the rc file we should append to for the given shell.
///
/// - Zsh  → `~/.zshrc`
/// - Bash → `~/.bashrc`
/// - `PowerShell` → `$PROFILE` if set by the host, otherwise the
///   conventional `Documents\PowerShell\Microsoft.PowerShell_profile.ps1`
///   under `$USERPROFILE` so the append still lands in the right place
///   when the binary is invoked outside an interactive PowerShell host
///   (e.g. from `cmd.exe`).
///
/// Returns `None` if the home directory cannot be determined and no
/// `$PROFILE` is set — in which case auto-setup quietly bails.
fn rc_file_for(shell: InitShell) -> Option<PathBuf> {
    match shell {
        InitShell::Zsh => dirs::home_dir().map(|home| home.join(".zshrc")),
        InitShell::Bash => dirs::home_dir().map(|home| home.join(".bashrc")),
        InitShell::Powershell => powershell_profile_path(),
    }
}

/// Resolves the `PowerShell` `$PROFILE` path. Prefers the host-
/// provided `PROFILE` env var (which `pwsh` exports), then falls back
/// to the conventional `Documents\PowerShell\…` layout rooted at the
/// user's home directory.
fn powershell_profile_path() -> Option<PathBuf> {
    if let Some(profile) = env::var_os("PROFILE") {
        let path = PathBuf::from(profile);
        if !path.as_os_str().is_empty() {
            return Some(path);
        }
    }
    dirs::home_dir().map(|home| {
        home.join("Documents")
            .join("PowerShell")
            .join("Microsoft.PowerShell_profile.ps1")
    })
}

/// Returns the single line that goes inside the fenced block for the
/// given shell. Byte-for-byte identical to what the shell installer
/// writes, so either code path producing the block results in the
/// same file contents.
fn eval_line_for(shell: InitShell) -> &'static str {
    match shell {
        InitShell::Zsh => "eval \"$(rep init zsh)\"",
        InitShell::Bash => "eval \"$(rep init bash)\"",
        InitShell::Powershell => "Invoke-Expression (& rep init powershell | Out-String)",
    }
}

/// Appends the fenced integration block to `rc_file` iff the marker
/// is absent. Creates parent directories and the file itself if they
/// don't exist yet (matching the installer scripts' behavior). All
/// I/O failures are surfaced as a short human-readable reason string
/// for the caller's stderr hint.
fn append_integration_if_missing(
    rc_file: &Path,
    shell: InitShell,
) -> Result<AutoSetupOutcome, String> {
    if rc_file.exists() {
        let existing = std::fs::read_to_string(rc_file)
            .map_err(|read_err| format!("read {}: {read_err}", rc_file.display()))?;
        if existing.contains(MARKER_START) {
            return Ok(AutoSetupOutcome::AlreadyPresent);
        }
        let needs_leading_newline = !existing.is_empty() && !existing.ends_with('\n');
        write_fenced_block(rc_file, shell, needs_leading_newline)?;
        return Ok(AutoSetupOutcome::Appended);
    }

    // File does not exist — create parent dirs first, then write.
    if let Some(parent) = rc_file.parent() {
        if !parent.as_os_str().is_empty() && !parent.exists() {
            std::fs::create_dir_all(parent)
                .map_err(|mkdir_err| format!("create {}: {mkdir_err}", parent.display()))?;
        }
    }
    write_fenced_block(rc_file, shell, false)?;
    Ok(AutoSetupOutcome::Appended)
}

/// Opens `rc_file` in append mode and writes the fenced block. The
/// `needs_leading_newline` flag inserts a separator newline when the
/// existing file does not already end in one, so the marker always
/// starts on its own line.
fn write_fenced_block(
    rc_file: &Path,
    shell: InitShell,
    needs_leading_newline: bool,
) -> Result<(), String> {
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(rc_file)
        .map_err(|open_err| format!("open {}: {open_err}", rc_file.display()))?;

    let eol = if matches!(shell, InitShell::Powershell) && cfg!(windows) {
        "\r\n"
    } else {
        "\n"
    };

    let mut block = String::new();
    if needs_leading_newline {
        block.push_str(eol);
    }
    block.push_str(eol);
    block.push_str(MARKER_START);
    block.push_str(eol);
    block.push_str(eval_line_for(shell));
    block.push_str(eol);
    block.push_str(MARKER_END);
    block.push_str(eol);

    file.write_all(block.as_bytes())
        .map_err(|write_err| format!("write {}: {write_err}", rc_file.display()))?;
    Ok(())
}

/// Returns the Bash and Zsh integration script content, stamped with
/// the running binary's version.
pub fn bash_integration_content() -> String {
    let null_device = std::path::Path::new("/dev").join("null");
    format!(
        r#"# RePortal shell integration — v{version}
# Generated by 'rep init bash|zsh'.
export REPORTAL_LOADED=1
rj() {{ cd "$(rep jump "$@")"; rep color; }}
ro() {{ rep open "$@"; rep color; }}
rjw() {{ cd "$(rep workspace jump "$@")"; rep color; }}
row() {{ rep workspace open "$@"; rep color; }}
rw() {{ rep web "$@"; }}
rr() {{ rep run "$@"; }}
_reportal_hook() {{ local _t; _t=$(rep color --mode prompt-hook 2>{null_device}); [ -n "$_t" ] && printf '\033]2;%s\007' "$_t"; }}
PROMPT_COMMAND="${{PROMPT_COMMAND:+$PROMPT_COMMAND;}}_reportal_hook"
"#,
        version = env!("CARGO_PKG_VERSION"),
        null_device = null_device.display(),
    )
}
