use std::fmt::Write;

use anyhow::Result;

use crate::config::{Config, Span};

use super::shared::{
    Features, STATUSES, ansi_pair, color_escape, features as detect_features, git_branch_icon,
    icon_pairs, layout, status_separator, status_template,
};

const RESET: &str = "\u{1b}[0m";

pub fn generate(config: &Config) -> Result<String> {
    let layout = layout(config)?;
    let os = layout.os;
    let path = layout.path;
    let Some(git) = layout.git else {
        return generate_no_git(config);
    };
    let prompt = layout.prompt_span;
    let features = detect_features(config, &layout);

    let os_style = ansi_pair(os.foreground.as_ref(), os.background.as_ref())?;
    let path_style = ansi_pair(path.foreground.as_ref(), path.background.as_ref())?;
    let git_clean_style = ansi_pair(git.foreground.as_ref(), git.background.as_ref())?;
    let git_dirty_style = ansi_pair(
        git.foreground.as_ref(),
        git.dirty_background.as_ref().or(git.background.as_ref()),
    )?;
    let os_tail = edge(
        os.tail.as_deref(),
        os.background.as_ref(),
        path.background.as_ref(),
        os.invert_tail,
    )?;
    let path_sep_clean = edge(
        path.separator.as_deref(),
        path.background.as_ref(),
        git.background.as_ref(),
        path.invert_separator,
    )?;
    let path_sep_dirty = edge(
        path.separator.as_deref(),
        path.background.as_ref(),
        git.dirty_background.as_ref().or(git.background.as_ref()),
        path.invert_separator,
    )?;
    let path_sep_last = edge(
        path.separator.as_deref().or(path.tail.as_deref()),
        path.background.as_ref(),
        None,
        path.invert_separator || path.invert_tail,
    )?;
    let git_sep_clean = edge(
        git.separator.as_deref(),
        git.background.as_ref(),
        None,
        git.invert_separator,
    )?;
    let git_sep_dirty = edge(
        git.separator.as_deref(),
        git.dirty_background.as_ref().or(git.background.as_ref()),
        None,
        git.invert_separator,
    )?;
    let prompt_ok_style = ansi_pair(prompt.success_foreground.as_ref(), None)?;
    let prompt_error_style = ansi_pair(prompt.error_foreground.as_ref(), None)?;

    let transient = config.transient.as_ref();
    let transient_enabled = transient.is_some_and(|transient| transient.enabled);
    let transient_template = transient
        .map(|transient| transient.template.as_str())
        .filter(|template| !template.is_empty())
        .unwrap_or(&prompt.template);
    let transient_ok_style = ansi_pair(
        transient
            .and_then(|transient| transient.success_foreground.as_ref())
            .or(prompt.success_foreground.as_ref()),
        transient.and_then(|transient| transient.background.as_ref()),
    )?;
    let transient_error_style = ansi_pair(
        transient
            .and_then(|transient| transient.error_foreground.as_ref())
            .or(prompt.error_foreground.as_ref()),
        transient.and_then(|transient| transient.background.as_ref()),
    )?;

    let mut script = String::new();
    writeln!(script, "# Generated by cailoxo. Do not edit by hand.")?;
    writeln!(script, "# shell: pwsh")?;
    writeln!(script)?;
    write_consts(
        &mut script,
        os,
        path,
        git,
        prompt,
        transient_template,
        &os_style,
        &path_style,
        &git_clean_style,
        &git_dirty_style,
        RESET,
        &os_tail,
        &path_sep_clean,
        &path_sep_dirty,
        &path_sep_last,
        &git_sep_clean,
        &git_sep_dirty,
        &prompt_ok_style,
        &prompt_error_style,
        &transient_ok_style,
        &transient_error_style,
        config.final_space,
        &features,
    )?;
    write_body(&mut script, transient_enabled, &features)?;
    Ok(script)
}

#[allow(clippy::too_many_arguments)]
fn write_consts(
    out: &mut String,
    os: &Span,
    path: &Span,
    git: &Span,
    prompt: &Span,
    transient_template: &str,
    os_style: &str,
    path_style: &str,
    git_clean_style: &str,
    git_dirty_style: &str,
    reset: &str,
    os_tail: &str,
    path_sep_clean: &str,
    path_sep_dirty: &str,
    path_sep_last: &str,
    git_sep_clean: &str,
    git_sep_dirty: &str,
    prompt_ok_style: &str,
    prompt_error_style: &str,
    transient_ok_style: &str,
    transient_error_style: &str,
    final_space: bool,
    features: &Features,
) -> Result<()> {
    let min_dirs = path.setting_str("min_dirs").unwrap_or("1");
    let min_dirs = min_dirs.parse::<usize>().unwrap_or(1);
    let path_url = path.setting_bool("url").unwrap_or(false)
        || path.setting_bool("hyperlink").unwrap_or(false);
    let osc7 = path.setting_bool("osc7").unwrap_or(false);
    let branch_icon = git_branch_icon(git)?;
    let fetch_upstream_icon = features.git_upstream_icon;
    let git_url = features.git_url;
    let fetch_remote = features.git_fetch;
    let fetch_remote_interval_ms = git
        .settings
        .get("fetch_remote_interval_ms")
        .and_then(|value| value.as_integer())
        .unwrap_or(60_000)
        .max(0);
    let fetch_remote_timeout_ms = git
        .settings
        .get("fetch_remote_timeout_ms")
        .and_then(|value| value.as_integer())
        .unwrap_or(5_000)
        .max(1);

    write_var(out, "CAILOXO_OS_TEMPLATE", &os.template)?;
    write_var(out, "CAILOXO_PATH_TEMPLATE", &path.template)?;
    write_var(out, "CAILOXO_GIT_TEMPLATE", &git.template)?;
    write_var(out, "CAILOXO_PROMPT_TEMPLATE", &prompt.template)?;
    write_var(out, "CAILOXO_TRANSIENT_TEMPLATE", transient_template)?;
    write_var(
        out,
        "CAILOXO_HOME_ICON",
        path.setting_str("home_icon").unwrap_or("~"),
    )?;
    write_var(
        out,
        "CAILOXO_FOLDER_ICON",
        path.setting_str("folder_icon").unwrap_or("…"),
    )?;
    write_var(
        out,
        "CAILOXO_EDGE_FORMAT",
        path.setting_str("edge_format").unwrap_or(""),
    )?;
    write_var(
        out,
        "CAILOXO_GITDIR_FORMAT",
        path.setting_str("gitdir_format").unwrap_or(""),
    )?;
    write_var(out, "CAILOXO_BRANCH_ICON", &branch_icon)?;
    write_var(out, "CAILOXO_STATUS_SEPARATOR", status_separator(git))?;
    writeln!(
        out,
        "$script:CAILOXO_GIT_STATUS = ${}",
        if features.git_status { "true" } else { "false" }
    )?;
    writeln!(
        out,
        "$script:CAILOXO_FETCH_UPSTREAM_ICON = ${}",
        if fetch_upstream_icon { "true" } else { "false" }
    )?;
    writeln!(
        out,
        "$script:CAILOXO_GIT_URL = ${}",
        if git_url { "true" } else { "false" }
    )?;
    writeln!(
        out,
        "$script:CAILOXO_FETCH_REMOTE = ${}",
        if fetch_remote { "true" } else { "false" }
    )?;
    writeln!(
        out,
        "$script:CAILOXO_FETCH_REMOTE_INTERVAL_MS = {fetch_remote_interval_ms}"
    )?;
    writeln!(
        out,
        "$script:CAILOXO_FETCH_REMOTE_TIMEOUT_MS = {fetch_remote_timeout_ms}"
    )?;
    writeln!(out, "$script:CAILOXO_MIN_DIRS = {min_dirs}")?;
    writeln!(
        out,
        "$script:CAILOXO_FINAL_SPACE = ${}",
        if final_space { "true" } else { "false" }
    )?;
    writeln!(
        out,
        "$script:CAILOXO_PATH_URL = ${}",
        if path_url { "true" } else { "false" }
    )?;
    writeln!(
        out,
        "$script:CAILOXO_OSC7 = ${}",
        if osc7 { "true" } else { "false" }
    )?;
    write_var(out, "CAILOXO_OS_STYLE", os_style)?;
    write_var(out, "CAILOXO_PATH_STYLE", path_style)?;
    write_var(out, "CAILOXO_GIT_CLEAN_STYLE", git_clean_style)?;
    write_var(out, "CAILOXO_GIT_DIRTY_STYLE", git_dirty_style)?;
    write_var(out, "CAILOXO_RESET", reset)?;
    write_var(out, "CAILOXO_OS_TAIL", os_tail)?;
    write_var(out, "CAILOXO_PATH_SEP_CLEAN", path_sep_clean)?;
    write_var(out, "CAILOXO_PATH_SEP_DIRTY", path_sep_dirty)?;
    write_var(out, "CAILOXO_PATH_SEP_LAST", path_sep_last)?;
    write_var(out, "CAILOXO_GIT_SEP_CLEAN", git_sep_clean)?;
    write_var(out, "CAILOXO_GIT_SEP_DIRTY", git_sep_dirty)?;
    write_var(out, "CAILOXO_PROMPT_OK_STYLE", prompt_ok_style)?;
    write_var(out, "CAILOXO_PROMPT_ERROR_STYLE", prompt_error_style)?;
    write_var(out, "CAILOXO_TRANSIENT_OK_STYLE", transient_ok_style)?;
    write_var(out, "CAILOXO_TRANSIENT_ERROR_STYLE", transient_error_style)?;

    write_status_templates(out, git)?;
    write_icon_map(out, "CAILOXO_OS_ICONS", "os", "nerdfont")?;
    if features.git_upstream_icon {
        write_icon_map(out, "CAILOXO_UPSTREAM_ICONS", "git_upstream", "nerdfont")?;
    }
    Ok(())
}

fn write_var(out: &mut String, name: &str, value: &str) -> Result<()> {
    writeln!(out, "$script:{name} = {}", ps_string(value))?;
    Ok(())
}

fn generate_no_git(config: &Config) -> Result<String> {
    let layout = layout(config)?;
    let features = detect_features(config, &layout);
    let os = layout.os;
    let path = layout.path;
    let prompt = layout.prompt_span;
    let os_style = ansi_pair(os.foreground.as_ref(), os.background.as_ref())?;
    let path_style = ansi_pair(path.foreground.as_ref(), path.background.as_ref())?;
    let os_tail = edge(
        os.tail.as_deref(),
        os.background.as_ref(),
        path.background.as_ref(),
        os.invert_tail,
    )?;
    let path_sep_last = edge(
        path.separator.as_deref().or(path.tail.as_deref()),
        path.background.as_ref(),
        None,
        path.invert_separator || path.invert_tail,
    )?;
    let prompt_ok_style = ansi_pair(prompt.success_foreground.as_ref(), None)?;
    let prompt_error_style = ansi_pair(prompt.error_foreground.as_ref(), None)?;
    let transient = config.transient.as_ref();
    let transient_enabled = transient.is_some_and(|transient| transient.enabled);
    let transient_template = transient
        .map(|transient| transient.template.as_str())
        .filter(|template| !template.is_empty())
        .unwrap_or(&prompt.template);
    let transient_ok_style = ansi_pair(
        transient
            .and_then(|transient| transient.success_foreground.as_ref())
            .or(prompt.success_foreground.as_ref()),
        transient.and_then(|transient| transient.background.as_ref()),
    )?;
    let transient_error_style = ansi_pair(
        transient
            .and_then(|transient| transient.error_foreground.as_ref())
            .or(prompt.error_foreground.as_ref()),
        transient.and_then(|transient| transient.background.as_ref()),
    )?;

    let mut out = String::new();
    writeln!(out, "# Generated by cailoxo. Do not edit by hand.")?;
    writeln!(out, "# shell: pwsh")?;
    writeln!(out)?;
    write_var(&mut out, "CAILOXO_OS_TEMPLATE", &os.template)?;
    write_var(&mut out, "CAILOXO_PATH_TEMPLATE", &path.template)?;
    write_var(&mut out, "CAILOXO_PROMPT_TEMPLATE", &prompt.template)?;
    write_var(&mut out, "CAILOXO_TRANSIENT_TEMPLATE", transient_template)?;
    write_var(
        &mut out,
        "CAILOXO_HOME_ICON",
        path.setting_str("home_icon").unwrap_or("~"),
    )?;
    write_var(
        &mut out,
        "CAILOXO_FOLDER_ICON",
        path.setting_str("folder_icon").unwrap_or("…"),
    )?;
    write_var(
        &mut out,
        "CAILOXO_EDGE_FORMAT",
        path.setting_str("edge_format").unwrap_or(""),
    )?;
    if features.gitdir_format {
        write_var(
            &mut out,
            "CAILOXO_GITDIR_FORMAT",
            path.setting_str("gitdir_format").unwrap_or(""),
        )?;
    }
    writeln!(
        out,
        "$script:CAILOXO_FINAL_SPACE = ${}",
        if config.final_space { "true" } else { "false" }
    )?;
    write_var(&mut out, "CAILOXO_OS_STYLE", &os_style)?;
    write_var(&mut out, "CAILOXO_PATH_STYLE", &path_style)?;
    write_var(&mut out, "CAILOXO_RESET", RESET)?;
    write_var(&mut out, "CAILOXO_OS_TAIL", &os_tail)?;
    write_var(&mut out, "CAILOXO_PATH_SEP_LAST", &path_sep_last)?;
    write_var(&mut out, "CAILOXO_PROMPT_OK_STYLE", &prompt_ok_style)?;
    write_var(&mut out, "CAILOXO_PROMPT_ERROR_STYLE", &prompt_error_style)?;
    write_var(&mut out, "CAILOXO_TRANSIENT_OK_STYLE", &transient_ok_style)?;
    write_var(
        &mut out,
        "CAILOXO_TRANSIENT_ERROR_STYLE",
        &transient_error_style,
    )?;
    write_icon_map(&mut out, "CAILOXO_OS_ICONS", "os", "nerdfont")?;

    let style_body = ps_style_body(&features);
    let plain_body = ps_plain_body(&features);
    let template_body = ps_template_body(&features, &["icon", "path", "home_icon", "folder_icon"]);
    let body = r#"
function Cailoxo-Plain-Template {
  param([string]$Text)
__CAILOXO_PLAIN_BODY__
}

function Cailoxo-Style-Template {
  param([string]$Text)
__CAILOXO_STYLE_BODY__
}

function Cailoxo-Format-Part {
  param([string]$Value, [string]$Format)
  if ([string]::IsNullOrEmpty($Format)) { return $Value }
  if ($Format.Contains('%s')) { return $Format.Replace('%s', $Value) }
  $Value
}

function Cailoxo-Os-Icon {
  $id = 'unknown'
  if ($IsWindows) { $id = 'windows' } elseif ($IsMacOS) { $id = 'macos' } elseif (Test-Path '/etc/os-release') {
    foreach ($line in Get-Content '/etc/os-release' -ErrorAction SilentlyContinue) { if ($line.StartsWith('ID=')) { $id = $line.Substring(3).Trim('"').ToLowerInvariant(); break } }
  }
  if ($script:CAILOXO_OS_ICONS.ContainsKey($id)) { return $script:CAILOXO_OS_ICONS[$id] }
  $script:CAILOXO_OS_ICONS['unknown']
}

function Cailoxo-Normalize-Path { param([string]$Path) if ($null -eq $Path) { return '' }; $Path.Replace('\', '/') }
function Cailoxo-Path-Separator { if ($IsWindows) { '\' } else { '/' } }
__CAILOXO_GIT_ROOT_FUNCTION__

function Cailoxo-Format-Path {
  param([string]$Path, [string]$GitRoot)
  $separator = Cailoxo-Path-Separator
  $prefix = ''; $rest = $Path; $absolute = $false
  if ($Path -eq '~') { return (Cailoxo-Format-Part $Path $script:CAILOXO_EDGE_FORMAT) } elseif ($Path.StartsWith('~/')) { $prefix = '~'; $rest = $Path.Substring(2) } elseif ($Path.StartsWith('…/')) { $prefix = '…'; $rest = $Path.Substring(2) } elseif ($Path.StartsWith('/')) { $absolute = $true; $rest = $Path.Substring(1) }
  $parts = @($rest.Split([char[]]@('/'), [StringSplitOptions]::RemoveEmptyEntries))
  if ($absolute -and $parts.Count -eq 0) { return (Cailoxo-Format-Part $separator $script:CAILOXO_EDGE_FORMAT) }
  $out = @()
  if ($prefix -ne '') { if ($prefix -eq '…') { $out += $prefix } else { $out += (Cailoxo-Format-Part $prefix $script:CAILOXO_EDGE_FORMAT) } }
  for ($i = 0; $i -lt $parts.Count; $i++) { $raw = $parts[$i]; $part = $raw; if ($GitRoot -ne '' -and $raw -eq $GitRoot) { $part = Cailoxo-Format-Part $part $script:CAILOXO_GITDIR_FORMAT }; if ($i -eq ($parts.Count - 1) -or ($i -eq 0 -and $out.Count -eq 0)) { $part = Cailoxo-Format-Part $part $script:CAILOXO_EDGE_FORMAT }; $out += $part }
  $joined = $out -join $separator
  if ($absolute) { $separator + $joined } else { $joined }
}

function Cailoxo-Home-Path { if (-not [string]::IsNullOrEmpty($HOME)) { return (Cailoxo-Normalize-Path $HOME) }; Cailoxo-Normalize-Path ([Environment]::GetFolderPath('UserProfile')) }

function Cailoxo-Shorten-Path {
  param([int]$Budget)
  $cwd = Cailoxo-Normalize-Path ((Get-Location).ProviderPath); $homePath = Cailoxo-Home-Path
  if ([string]::Equals($cwd, $homePath, [StringComparison]::OrdinalIgnoreCase)) { $display = '~' } elseif ($homePath -ne '' -and $cwd.StartsWith($homePath + '/', [StringComparison]::OrdinalIgnoreCase)) { $display = '~' + $cwd.Substring($homePath.Length) } else { $display = $cwd }
  if ($display.Length -le $Budget) { return $display }
  if ($display.StartsWith('~/')) { $prefix = '~'; $rest = $display.Substring(2) } elseif ($display.StartsWith('/')) { $prefix = ''; $rest = $display.Substring(1) } else { $prefix = ''; $rest = $display }
  $parts = @($rest.Split([char[]]@('/'), [StringSplitOptions]::RemoveEmptyEntries)); if ($parts.Count -eq 0) { return $display }
  $current = $parts[$parts.Count - 1]; if ($Budget -le 4) { return "…/$current" }
  if ($parts.Count -ge 2) { $candidate = "$(if ($prefix -ne '') { "$prefix/" })…/$($parts[$parts.Count - 2])/$current"; if ($candidate.Length -le $Budget) { return $candidate } }
  $candidate = "$(if ($prefix -ne '') { "$prefix/" })…/$current"; if ($candidate.Length -le $Budget) { return $candidate }
  "…/$current"
}

function Cailoxo-Apply-Template {
  param([string]$Template, [hashtable]$Vars)
  $tpl = $Template
__CAILOXO_TEMPLATE_BODY__
  $tpl
}

function Cailoxo-Path-Template {
  param([hashtable]$Vars)
  $marker = '{{ if home }}'
  if (-not $script:CAILOXO_PATH_TEMPLATE.Contains($marker)) { return (Cailoxo-Apply-Template $script:CAILOXO_PATH_TEMPLATE $Vars) }
  $start = $script:CAILOXO_PATH_TEMPLATE.IndexOf($marker); $before = $script:CAILOXO_PATH_TEMPLATE.Substring(0, $start); $rest = $script:CAILOXO_PATH_TEMPLATE.Substring($start + $marker.Length); $else = $rest.IndexOf('{{ else }}'); $end = $rest.IndexOf('{{ end }}')
  if ($else -lt 0 -or $end -lt 0) { return (Cailoxo-Apply-Template $script:CAILOXO_PATH_TEMPLATE $Vars) }
  $homePart = $rest.Substring(0, $else); $otherPart = $rest.Substring($else + '{{ else }}'.Length, $end - ($else + '{{ else }}'.Length)); $after = $rest.Substring($end + '{{ end }}'.Length); $body = if ($Vars['home']) { $homePart } else { $otherPart }
  Cailoxo-Apply-Template ($before + $body + $after) $Vars
}

function Cailoxo-Render-Full {
  param([int]$LastStatus)
  $osText = Cailoxo-Apply-Template $script:CAILOXO_OS_TEMPLATE @{ icon = (Cailoxo-Os-Icon) }
  $width = 80; try { $width = [Console]::WindowWidth } catch {}; if ($width -le 0) { $width = 80 }
  $fixed = (Cailoxo-Plain-Template $osText).Length + $script:CAILOXO_HOME_ICON.Length + $script:CAILOXO_FOLDER_ICON.Length + 10
__CAILOXO_GIT_ROOT_SET__
  $path = Cailoxo-Format-Path (Cailoxo-Shorten-Path ([Math]::Max(1, $width - $fixed))) __CAILOXO_GIT_ROOT_ARG__
  $homePath = Cailoxo-Home-Path; $isHome = [string]::Equals((Cailoxo-Normalize-Path ((Get-Location).ProviderPath)), $homePath, [StringComparison]::OrdinalIgnoreCase)
  $pathText = Cailoxo-Path-Template @{ path = $path; home = $isHome; home_icon = $script:CAILOXO_HOME_ICON; folder_icon = $script:CAILOXO_FOLDER_ICON }
  $promptStyle = if ($LastStatus -eq 0) { $script:CAILOXO_PROMPT_OK_STYLE } else { $script:CAILOXO_PROMPT_ERROR_STYLE }
  $suffix = if ($script:CAILOXO_FINAL_SPACE) { ' ' } else { '' }
  $script:CAILOXO_OS_STYLE + (Cailoxo-Style-Template $osText) + $script:CAILOXO_RESET + $script:CAILOXO_OS_TAIL + $script:CAILOXO_PATH_STYLE + (Cailoxo-Style-Template $pathText) + $script:CAILOXO_RESET + $script:CAILOXO_PATH_SEP_LAST + "`n" + $promptStyle + (Cailoxo-Style-Template $script:CAILOXO_PROMPT_TEMPLATE) + $script:CAILOXO_RESET + $suffix
}

function Cailoxo-Render-Transient { param([int]$LastStatus) $promptStyle = if ($LastStatus -eq 0) { $script:CAILOXO_TRANSIENT_OK_STYLE } else { $script:CAILOXO_TRANSIENT_ERROR_STYLE }; $promptStyle + (Cailoxo-Style-Template $script:CAILOXO_TRANSIENT_TEMPLATE) + $script:CAILOXO_RESET }

function prompt { $originalSuccess = $?; $originalLastExitCode = $global:LASTEXITCODE; $lastStatus = if ($originalSuccess) { 0 } elseif ($null -ne $originalLastExitCode) { [int]$originalLastExitCode } else { 1 }; $output = Cailoxo-Render-Full $lastStatus; try { Set-PSReadLineOption -ExtraPromptLineCount ((($output -split "`n").Count) - 1) } catch {}; $global:LASTEXITCODE = $originalLastExitCode; $output }
"#;
    out.push_str(
        &body
            .replace("__CAILOXO_STYLE_BODY__", &style_body)
            .replace("__CAILOXO_PLAIN_BODY__", &plain_body)
            .replace("__CAILOXO_TEMPLATE_BODY__", &template_body)
            .replace(
                "__CAILOXO_GIT_ROOT_FUNCTION__",
                if features.gitdir_format {
                    r#"
function Cailoxo-Git-Root-Name {
  $root = & git rev-parse --show-toplevel 2>$null
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace(($root -join ''))) { return '' }
  Split-Path -Leaf (($root -join "`n").Trim())
}
"#
                } else {
                    ""
                },
            )
            .replace(
                "__CAILOXO_GIT_ROOT_SET__",
                if features.gitdir_format {
                    "  $gitRoot = Cailoxo-Git-Root-Name"
                } else {
                    ""
                },
            )
            .replace(
                "__CAILOXO_GIT_ROOT_ARG__",
                if features.gitdir_format {
                    "$gitRoot"
                } else {
                    "''"
                },
            ),
    );
    if transient_enabled {
        out.push_str(
            r#"
function Set-CailoxoTransientPrompt { try { [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } catch {} }
try { Set-PSReadLineKeyHandler -Key Enter -BriefDescription 'CailoxoEnterKeyHandler' -ScriptBlock { try { Set-CailoxoTransientPrompt } finally { [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine() } } } catch {}
"#,
        );
    }
    Ok(out)
}

fn ps_style_body(features: &Features) -> String {
    let mut out = String::from("  $esc = [char]27\n  $Text");
    for tag in &features.tags {
        write!(
            out,
            ".Replace({}, \"$esc{}\").Replace({}, \"$esc{}\")",
            ps_string(tag.open),
            tag.on.trim_start_matches("\\e"),
            ps_string(tag.close),
            tag.off.trim_start_matches("\\e")
        )
        .unwrap();
    }
    out.push('\n');
    out
}

fn ps_plain_body(features: &Features) -> String {
    let mut out = String::from("  $Text");
    for tag in &features.tags {
        write!(
            out,
            ".Replace({}, '').Replace({}, '')",
            ps_string(tag.open),
            ps_string(tag.close)
        )
        .unwrap();
    }
    out.push('\n');
    out
}

fn ps_template_body(features: &Features, names: &[&str]) -> String {
    let mut out = String::new();
    for name in names {
        let enabled = match *name {
            "icon" => features.var_icon,
            "path" => features.var_path,
            "home_icon" => features.var_home_icon,
            "folder_icon" => features.var_folder_icon,
            _ => false,
        };
        if enabled {
            writeln!(
                out,
                "  if ($Vars.ContainsKey('{0}')) {{ $tpl = $tpl.Replace('{{{{ {0} }}}}', [string]$Vars['{0}']) }}",
                name
            )
            .unwrap();
        }
    }
    out
}

fn write_status_templates(out: &mut String, git: &Span) -> Result<()> {
    writeln!(out, "$script:CAILOXO_STATUS_TEMPLATES = @{{")?;
    for status in STATUSES {
        writeln!(
            out,
            "  {} = {}",
            ps_string(status),
            ps_string(&status_template(git, status)?)
        )?;
    }
    writeln!(out, "}}")?;
    Ok(())
}

fn write_icon_map(out: &mut String, name: &str, group: &str, set: &str) -> Result<()> {
    writeln!(out, "$script:{name} = @{{")?;
    for (key, value) in icon_pairs(group, set)? {
        writeln!(out, "  {} = {}", ps_string(&key), ps_string(&value))?;
    }
    writeln!(out, "}}")?;
    Ok(())
}

fn write_body(out: &mut String, transient_enabled: bool, features: &Features) -> Result<()> {
    let style_body = ps_style_body(features);
    let plain_body = ps_plain_body(features);
    let body = r#"
$script:CAILOXO_PROMPT_TYPE = 'primary'
$script:CAILOXO_FETCH_PROCESS = $null
$script:CAILOXO_FETCH_EVENT = $null
$script:CAILOXO_FETCH_LAST_KEY = ''
$script:CAILOXO_FETCH_LAST_START_MS = 0
$script:CAILOXO_FETCH_DEADLINE_MS = 0
$script:CAILOXO_FETCH_ROOT = ''
$script:CAILOXO_FETCH_UPSTREAM_REF = ''
$script:CAILOXO_FETCH_BEFORE = ''
$script:CAILOXO_FETCH_REPAINT_PENDING = $false
$script:CAILOXO_RENDERING = $false
$script:CAILOXO_FETCH_READY = $false

function Cailoxo-Plain-Template {
  param([string]$Text)
__CAILOXO_PLAIN_BODY__
}

function Cailoxo-Style-Template {
  param([string]$Text)
__CAILOXO_STYLE_BODY__
}

function Cailoxo-Format-Part {
  param([string]$Value, [string]$Format)
  if ([string]::IsNullOrEmpty($Format)) { return $Value }
  if ($Format.Contains('%s')) { return $Format.Replace('%s', $Value) }
  $Value
}

function Cailoxo-Url-Escape {
  param([string]$Path)
  $Path.Replace('\', '/').Replace('%', '%25').Replace(' ', '%20').Replace('#', '%23').Replace('?', '%3F').Replace(';', '%3B')
}

function Cailoxo-HostName {
  if (-not [string]::IsNullOrWhiteSpace($env:COMPUTERNAME)) { return $env:COMPUTERNAME }
  if (-not [string]::IsNullOrWhiteSpace($env:HOSTNAME)) { return $env:HOSTNAME }
  try { return ([System.Net.Dns]::GetHostName()) } catch { return '' }
}

function Cailoxo-File-Url {
  $path = Cailoxo-Url-Escape ((Get-Location).ProviderPath)
  if ($path -match '^[A-Za-z]:/') { return "file:///$path" }
  "file://$(Cailoxo-HostName)$path"
}

function Cailoxo-Osc7 {
  if (-not $script:CAILOXO_OSC7) { return '' }
  "$([char]27)]7;$(Cailoxo-File-Url)$([char]27)\"
}

function Cailoxo-Path-Url-Start {
  if (-not $script:CAILOXO_PATH_URL) { return '' }
  "$([char]27)]8;;$(Cailoxo-File-Url)$([char]27)\"
}

function Cailoxo-Path-Url-End {
  if (-not $script:CAILOXO_PATH_URL) { return '' }
  "$([char]27)]8;;$([char]27)\"
}

function Cailoxo-Normalize-Path {
  param([string]$Path)
  if ($null -eq $Path) { return '' }
  $Path.Replace('\', '/')
}

function Cailoxo-Home-Path {
  if (-not [string]::IsNullOrEmpty($HOME)) { return (Cailoxo-Normalize-Path $HOME) }
  Cailoxo-Normalize-Path ([Environment]::GetFolderPath('UserProfile'))
}

function Cailoxo-Path-Separator {
  if ($IsWindows) { '\' } else { '/' }
}

function Cailoxo-Git-Root-Name {
  $root = & git rev-parse --show-toplevel 2>$null
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace(($root -join ''))) { return '' }
  Split-Path -Leaf (($root -join "`n").Trim())
}

function Cailoxo-Format-Path {
  param([string]$Path, [string]$GitRoot)
  $separator = Cailoxo-Path-Separator
  $prefix = ''
  $rest = $Path
  $absolute = $false
  if ($Path -eq '~') {
    return (Cailoxo-Format-Part $Path $script:CAILOXO_EDGE_FORMAT)
  } elseif ($Path.StartsWith('~/')) {
    $prefix = '~'
    $rest = $Path.Substring(2)
  } elseif ($Path.StartsWith('…/')) {
    $prefix = '…'
    $rest = $Path.Substring(2)
  } elseif ($Path.StartsWith('/')) {
    $absolute = $true
    $rest = $Path.Substring(1)
  }

  $parts = @($rest.Split([char[]]@('/'), [StringSplitOptions]::RemoveEmptyEntries))
  if ($absolute -and $parts.Count -eq 0) { return (Cailoxo-Format-Part $separator $script:CAILOXO_EDGE_FORMAT) }

  $out = @()
  if ($prefix -ne '') {
    if ($prefix -eq '…') { $out += $prefix } else { $out += (Cailoxo-Format-Part $prefix $script:CAILOXO_EDGE_FORMAT) }
  }

  for ($i = 0; $i -lt $parts.Count; $i++) {
    $raw = $parts[$i]
    $part = $raw
    if ($GitRoot -ne '' -and $raw -eq $GitRoot) { $part = Cailoxo-Format-Part $part $script:CAILOXO_GITDIR_FORMAT }
    if ($i -eq ($parts.Count - 1) -or ($i -eq 0 -and $out.Count -eq 0)) { $part = Cailoxo-Format-Part $part $script:CAILOXO_EDGE_FORMAT }
    $out += $part
  }

  $joined = $out -join $separator
  if ($absolute) { $separator + $joined } else { $joined }
}

function Cailoxo-Shorten-Path {
  param([int]$Budget)
  $cwd = Cailoxo-Normalize-Path ((Get-Location).ProviderPath)
  $homePath = Cailoxo-Home-Path
  if ([string]::Equals($cwd, $homePath, [StringComparison]::OrdinalIgnoreCase)) {
    $display = '~'
  } elseif ($homePath -ne '' -and $cwd.StartsWith($homePath + '/', [StringComparison]::OrdinalIgnoreCase)) {
    $display = '~' + $cwd.Substring($homePath.Length)
  } else {
    $display = $cwd
  }

  if ($display.Length -le $Budget) { return $display }
  if ($display.StartsWith('~/')) {
    $prefix = '~'
    $rest = $display.Substring(2)
  } elseif ($display.StartsWith('/')) {
    $prefix = ''
    $rest = $display.Substring(1)
  } else {
    $prefix = ''
    $rest = $display
  }

  $parts = @($rest.Split([char[]]@('/'), [StringSplitOptions]::RemoveEmptyEntries))
  if ($parts.Count -eq 0) { return $display }
  $current = $parts[$parts.Count - 1]
  if ($Budget -le 4) { return "…/$current" }
  if ($parts.Count -ge 2) {
    $candidate = "$(if ($prefix -ne '') { "$prefix/" })…/$($parts[$parts.Count - 2])/$current"
    if ($candidate.Length -le $Budget) { return $candidate }
  }
  $candidate = "$(if ($prefix -ne '') { "$prefix/" })…/$current"
  if ($candidate.Length -le $Budget) { return $candidate }
  "…/$current"
}

function Cailoxo-Os-Icon {
  $id = 'unknown'
  if ($IsWindows) {
    $id = 'windows'
  } elseif ($IsMacOS) {
    $id = 'macos'
  } elseif (Test-Path '/etc/os-release') {
    foreach ($line in Get-Content '/etc/os-release' -ErrorAction SilentlyContinue) {
      if ($line.StartsWith('ID=')) {
        $id = $line.Substring(3).Trim('"').ToLowerInvariant()
        break
      }
    }
  }
  if ($script:CAILOXO_OS_ICONS.ContainsKey($id)) { return $script:CAILOXO_OS_ICONS[$id] }
  $script:CAILOXO_OS_ICONS['unknown']
}

function Cailoxo-Upstream-Provider {
  param([string]$Url)
  $normalized = $Url.ToLowerInvariant()
  if ($normalized.Contains('github.com')) { return 'github' }
  if ($normalized.Contains('gitlab.com')) { return 'gitlab' }
  if ($normalized.Contains('bitbucket.org')) { return 'bitbucket' }
  if ($normalized.Contains('codeberg.org')) { return 'codeberg' }
  if ($normalized.Contains('gitea')) { return 'gitea' }
  if ($normalized.Contains('dev.azure.com') -or $normalized.Contains('visualstudio.com')) { return 'azure_devops' }
  ''
}

function Cailoxo-Clean-Git-Url {
  param([string]$RemoteUrl)
  $url = $RemoteUrl.Trim() -replace '\.git/?$', '' -replace '/$', ''
  if ([string]::IsNullOrWhiteSpace($url)) { return '' }
  if ($url.StartsWith('http://') -or $url.StartsWith('https://')) { return $url }

  if ($url -match '^git@ssh\.dev\.azure\.com:v3/(?<org>[^/]+)/(?<project>[^/]+)/(?<repo>[^/]+)$') {
    return "https://dev.azure.com/$($Matches.org)/$($Matches.project)/_git/$($Matches.repo)"
  }

  if ($url -match '^[A-Za-z][A-Za-z0-9+.-]*://') {
    $rest = $url -replace '^[A-Za-z][A-Za-z0-9+.-]*://', ''
    if ($rest.Contains('@')) { $rest = ($rest -split '@')[-1] }
    $slash = $rest.IndexOf('/')
    if ($slash -lt 0) { return '' }
    $host = $rest.Substring(0, $slash).Split(':')[0]
    $path = $rest.Substring($slash + 1)
    if ($host -eq 'ssh.dev.azure.com' -and $path -match '^v3/(?<org>[^/]+)/(?<project>[^/]+)/(?<repo>[^/]+)$') {
      return "https://dev.azure.com/$($Matches.org)/$($Matches.project)/_git/$($Matches.repo)"
    }
    if ($host -ne '' -and $path -ne '') { return "https://$host/$path" }
  }

  if ($url -match '^[^@]+@(?<host>[^:]+):(?<path>.+)$') {
    return "https://$($Matches.host)/$($Matches.path)"
  }

  if ($url -match '^(?<host>[A-Za-z0-9.-]+):(?<path>.+)$') {
    return "https://$($Matches.host)/$($Matches.path)"
  }

  ''
}

function Cailoxo-Git-Url-Start {
  param([string]$Url)
  if (-not $script:CAILOXO_GIT_URL -or [string]::IsNullOrWhiteSpace($Url)) { return '' }
  "$([char]27)]8;;$Url$([char]27)\"
}

function Cailoxo-Git-Url-End {
  param([string]$Url)
  if (-not $script:CAILOXO_GIT_URL -or [string]::IsNullOrWhiteSpace($Url)) { return '' }
  "$([char]27)]8;;$([char]27)\"
}

function Cailoxo-Upstream-Info {
  param([string]$Branch)
  $info = @{ upstream = ''; upstream_icon = ''; upstream_url = '' }
  if (-not $script:CAILOXO_FETCH_UPSTREAM_ICON -and -not $script:CAILOXO_GIT_URL) { return $info }
  $remote = Cailoxo-Remote-Name $Branch
  $url = (& git config --get "remote.$remote.url" 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($url)) { return $info }
  $rawUrl = $url.Trim()
  $upstream = Cailoxo-Upstream-Provider $rawUrl
  $icon = if ($script:CAILOXO_FETCH_UPSTREAM_ICON -and $upstream -ne '' -and $script:CAILOXO_UPSTREAM_ICONS.ContainsKey($upstream)) { $script:CAILOXO_UPSTREAM_ICONS[$upstream] } else { '' }
  @{ upstream = $upstream; upstream_icon = $icon; upstream_url = (Cailoxo-Clean-Git-Url $rawUrl) }
}

function Cailoxo-Remote-Name {
  param([string]$Branch)
  $remote = (& git config --get "branch.$Branch.remote" 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($remote)) { return 'origin' }
  $remote.Trim()
}

function Cailoxo-Now-Ms {
  [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
}

function Cailoxo-Request-Repaint {
  if ($script:CAILOXO_RENDERING) {
    $script:CAILOXO_FETCH_REPAINT_PENDING = $true
    return
  }
  try {
    $line = $null
    $cursor = $null
    [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
    [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
    $script:CAILOXO_FETCH_REPAINT_PENDING = $false
  } catch {
    $script:CAILOXO_FETCH_REPAINT_PENDING = $true
  }
}

function Cailoxo-Poll-Fetch {
  param([switch]$Repaint)
  if ($null -ne $script:CAILOXO_FETCH_PROCESS) {
    $now = Cailoxo-Now-Ms
    if (-not $script:CAILOXO_FETCH_PROCESS.HasExited -and $script:CAILOXO_FETCH_DEADLINE_MS -gt 0 -and $now -ge $script:CAILOXO_FETCH_DEADLINE_MS) {
      try { $script:CAILOXO_FETCH_PROCESS.Kill($true) } catch { try { $script:CAILOXO_FETCH_PROCESS.Kill() } catch {} }
    }
    if ($script:CAILOXO_FETCH_PROCESS.HasExited) {
      $exitCode = $script:CAILOXO_FETCH_PROCESS.ExitCode
      try { $script:CAILOXO_FETCH_PROCESS.Dispose() } catch {}
      $script:CAILOXO_FETCH_PROCESS = $null
      $script:CAILOXO_FETCH_DEADLINE_MS = 0
      if ($null -ne $script:CAILOXO_FETCH_EVENT) {
        try { Unregister-Event -SubscriptionId $script:CAILOXO_FETCH_EVENT.Id -ErrorAction SilentlyContinue } catch {}
        $script:CAILOXO_FETCH_EVENT = $null
      }
      if ($exitCode -eq 0 -and $script:CAILOXO_FETCH_UPSTREAM_REF -ne '') {
        $after = (& git -C $script:CAILOXO_FETCH_ROOT rev-parse --verify $script:CAILOXO_FETCH_UPSTREAM_REF 2>$null) -join ''
        if ($LASTEXITCODE -eq 0 -and $after.Trim() -ne '' -and $after.Trim() -ne $script:CAILOXO_FETCH_BEFORE) {
          $script:CAILOXO_FETCH_REPAINT_PENDING = $true
        }
      }
      $script:CAILOXO_FETCH_UPSTREAM_REF = ''
      $script:CAILOXO_FETCH_BEFORE = ''
    }
  } else {
    if ($null -ne $script:CAILOXO_FETCH_EVENT) {
      try { Unregister-Event -SubscriptionId $script:CAILOXO_FETCH_EVENT.Id -ErrorAction SilentlyContinue } catch {}
      $script:CAILOXO_FETCH_EVENT = $null
    }
  }
  if ($Repaint -and $script:CAILOXO_FETCH_REPAINT_PENDING) { Cailoxo-Request-Repaint }
}

function Cailoxo-Start-Fetch {
  param([string]$Branch)
  if (-not $script:CAILOXO_FETCH_REMOTE) { return }
  if (-not $script:CAILOXO_FETCH_READY) { return }
  Cailoxo-Poll-Fetch
  if ($null -ne $script:CAILOXO_FETCH_PROCESS) { return }

  $remote = Cailoxo-Remote-Name $Branch
  if ([string]::IsNullOrWhiteSpace($remote)) { return }
  $root = (& git rev-parse --show-toplevel 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($root)) { return }
  $root = $root.Trim()

  $now = Cailoxo-Now-Ms
  $key = "$root|$remote"
  if ($script:CAILOXO_FETCH_LAST_KEY -eq $key -and ($now - $script:CAILOXO_FETCH_LAST_START_MS) -lt $script:CAILOXO_FETCH_REMOTE_INTERVAL_MS) { return }
  $upstreamRef = (& git -C $root rev-parse --symbolic-full-name "$Branch@{upstream}" 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($upstreamRef)) { return }
  $upstreamRef = $upstreamRef.Trim()
  $before = (& git -C $root rev-parse --verify $upstreamRef 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($before)) { $before = '' } else { $before = $before.Trim() }
  $script:CAILOXO_FETCH_LAST_KEY = $key
  $script:CAILOXO_FETCH_LAST_START_MS = $now
  $script:CAILOXO_FETCH_ROOT = $root
  $script:CAILOXO_FETCH_UPSTREAM_REF = $upstreamRef
  $script:CAILOXO_FETCH_BEFORE = $before

  $psi = [System.Diagnostics.ProcessStartInfo]::new()
  $psi.FileName = 'git'
  $psi.WorkingDirectory = $root
  $psi.UseShellExecute = $false
  $psi.RedirectStandardOutput = $true
  $psi.RedirectStandardError = $true
  $psi.CreateNoWindow = $true
  $psi.ArgumentList.Add('fetch')
  $psi.ArgumentList.Add('--quiet')
  $psi.ArgumentList.Add('--no-tags')
  $psi.ArgumentList.Add($remote)
  try {
    $script:CAILOXO_FETCH_PROCESS = [System.Diagnostics.Process]::Start($psi)
    $script:CAILOXO_FETCH_PROCESS.EnableRaisingEvents = $true
    try { $script:CAILOXO_FETCH_PROCESS.BeginOutputReadLine() } catch {}
    try { $script:CAILOXO_FETCH_PROCESS.BeginErrorReadLine() } catch {}
    $script:CAILOXO_FETCH_DEADLINE_MS = $now + $script:CAILOXO_FETCH_REMOTE_TIMEOUT_MS
    if ($null -ne $script:CAILOXO_FETCH_EVENT) { Unregister-Event -SubscriptionId $script:CAILOXO_FETCH_EVENT.Id -ErrorAction SilentlyContinue }
    $script:CAILOXO_FETCH_EVENT = Register-ObjectEvent -InputObject $script:CAILOXO_FETCH_PROCESS -EventName Exited -Action {
      Cailoxo-Poll-Fetch
      if ($script:CAILOXO_FETCH_REPAINT_PENDING -and -not $script:CAILOXO_RENDERING) {
        try {
          $line = $null
          $cursor = $null
          [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)
          [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt()
          $script:CAILOXO_FETCH_REPAINT_PENDING = $false
        } catch {}
      }
    }
  } catch {
    $script:CAILOXO_FETCH_PROCESS = $null
    $script:CAILOXO_FETCH_DEADLINE_MS = 0
    $script:CAILOXO_FETCH_UPSTREAM_REF = ''
    $script:CAILOXO_FETCH_BEFORE = ''
  }
}

function Cailoxo-Apply-Template {
  param([string]$Template, [hashtable]$Vars)
  $tpl = $Template
  $marker = '{{ if status }}'
  $endMarker = '{{ end }}'
  $elseMarker = '{{ else }}'
  while ($tpl.Contains($marker)) {
    $start = $tpl.IndexOf($marker)
    $before = $tpl.Substring(0, $start)
    $rest = $tpl.Substring($start + $marker.Length)
    $end = $rest.IndexOf($endMarker)
    if ($end -lt 0) { break }
    $body = $rest.Substring(0, $end)
    $after = $rest.Substring($end + $endMarker.Length)
    $else = $body.IndexOf($elseMarker)
    if ($else -ge 0) {
      $thenPart = $body.Substring(0, $else)
      $elsePart = $body.Substring($else + $elseMarker.Length)
    } else {
      $thenPart = $body
      $elsePart = ''
    }
    $status = if ($Vars.ContainsKey('status')) { [string]$Vars['status'] } else { '' }
    $tpl = $before + $(if ($status -ne '') { $thenPart } else { $elsePart }) + $after
  }

  foreach ($key in @('icon', 'path', 'home_icon', 'folder_icon', 'status', 'branch_icon', 'upstream_icon', 'upstream', 'upstream_url', 'branch')) {
    $value = if ($Vars.ContainsKey($key)) { [string]$Vars[$key] } else { '' }
    $tpl = $tpl.Replace("{{ $key }}", $value)
  }
  $tpl
}

function Cailoxo-Path-Template {
  param([hashtable]$Vars)
  $marker = '{{ if home }}'
  if (-not $script:CAILOXO_PATH_TEMPLATE.Contains($marker)) {
    return (Cailoxo-Apply-Template $script:CAILOXO_PATH_TEMPLATE $Vars)
  }
  $start = $script:CAILOXO_PATH_TEMPLATE.IndexOf($marker)
  $before = $script:CAILOXO_PATH_TEMPLATE.Substring(0, $start)
  $rest = $script:CAILOXO_PATH_TEMPLATE.Substring($start + $marker.Length)
  $else = $rest.IndexOf('{{ else }}')
  $end = $rest.IndexOf('{{ end }}')
  if ($else -lt 0 -or $end -lt 0) { return (Cailoxo-Apply-Template $script:CAILOXO_PATH_TEMPLATE $Vars) }
  $homePart = $rest.Substring(0, $else)
  $otherPart = $rest.Substring($else + '{{ else }}'.Length, $end - ($else + '{{ else }}'.Length))
  $after = $rest.Substring($end + '{{ end }}'.Length)
  $body = if ($Vars['home']) { $homePart } else { $otherPart }
  Cailoxo-Apply-Template ($before + $body + $after) $Vars
}

function Cailoxo-Status-Item {
  param([string]$Name, [int]$Count, [string]$Action)
  if ($Name -eq 'action') {
    if ([string]::IsNullOrEmpty($Action)) { return '' }
    return $script:CAILOXO_STATUS_TEMPLATES[$Name].Replace('{{ action }}', $Action)
  }
  if ($Count -le 0) { return '' }
  $script:CAILOXO_STATUS_TEMPLATES[$Name].Replace('{{ count }}', [string]$Count)
}

function Cailoxo-Git-Path {
  param([string]$Name)
  $path = (& git rev-parse --git-path $Name 2>$null) -join ''
  if ($LASTEXITCODE -ne 0) { return '' }
  $path.Trim()
}

function Cailoxo-Read-Git-File {
  param([string]$Path)
  if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path)) { return '' }
  try { ((Get-Content -LiteralPath $Path -Raw -ErrorAction Stop) -as [string]).Trim() } catch { '' }
}

function Cailoxo-Git-Action-With-Progress {
  param([string]$Action, [string]$Dir)
  $msgnum = Cailoxo-Read-Git-File (Join-Path $Dir 'msgnum')
  $end = Cailoxo-Read-Git-File (Join-Path $Dir 'end')
  $next = if ($msgnum -ne '') { $msgnum } else { Cailoxo-Read-Git-File (Join-Path $Dir 'next') }
  $last = if ($end -ne '') { $end } else { Cailoxo-Read-Git-File (Join-Path $Dir 'last') }
  if ($next -ne '' -and $last -ne '') { return "$Action $next/$last" }
  $Action
}

function Cailoxo-Git-Action {
  $rebaseMerge = Cailoxo-Git-Path 'rebase-merge'
  if ($rebaseMerge -ne '' -and (Test-Path -LiteralPath $rebaseMerge)) {
    $action = if (Test-Path -LiteralPath (Join-Path $rebaseMerge 'interactive')) { 'rebase-i' } else { 'rebase-m' }
    return (Cailoxo-Git-Action-With-Progress $action $rebaseMerge)
  }

  $rebaseApply = Cailoxo-Git-Path 'rebase-apply'
  if ($rebaseApply -ne '' -and (Test-Path -LiteralPath $rebaseApply)) {
    $action = if (Test-Path -LiteralPath (Join-Path $rebaseApply 'rebasing')) { 'rebase' } elseif (Test-Path -LiteralPath (Join-Path $rebaseApply 'applying')) { 'am' } else { 'am/rebase' }
    return (Cailoxo-Git-Action-With-Progress $action $rebaseApply)
  }

  $mergeHead = Cailoxo-Git-Path 'MERGE_HEAD'
  if ($mergeHead -ne '' -and (Test-Path -LiteralPath $mergeHead)) { return 'merge' }

  $revertHead = Cailoxo-Git-Path 'REVERT_HEAD'
  if ($revertHead -ne '' -and (Test-Path -LiteralPath $revertHead)) {
    $sequencer = Cailoxo-Git-Path 'sequencer'
    if ($sequencer -ne '' -and (Test-Path -LiteralPath $sequencer)) { return 'revert-seq' }
    return 'revert'
  }

  $cherryHead = Cailoxo-Git-Path 'CHERRY_PICK_HEAD'
  if ($cherryHead -ne '' -and (Test-Path -LiteralPath $cherryHead)) {
    $sequencer = Cailoxo-Git-Path 'sequencer'
    if ($sequencer -ne '' -and (Test-Path -LiteralPath $sequencer)) { return 'cherry-seq' }
    return 'cherry'
  }

  $bisectLog = Cailoxo-Git-Path 'BISECT_LOG'
  if ($bisectLog -ne '' -and (Test-Path -LiteralPath $bisectLog)) { return 'bisect' }
  ''
}

function Cailoxo-Count-Command {
  param([scriptblock]$Block)
  $raw = & $Block 2>$null
  if ($LASTEXITCODE -ne 0) { return 0 }
  $value = 0
  if ([int]::TryParse((($raw -join '').Trim()), [ref]$value)) { return $value }
  0
}

function Cailoxo-Git-Info {
  $empty = @{ branch = ''; status = ''; dirty = $false; upstream = ''; upstream_icon = ''; upstream_url = '' }
  $inside = (& git rev-parse --is-inside-work-tree 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or $inside.Trim() -ne 'true') { return $empty }

  $branch = (& git symbolic-ref --quiet --short HEAD 2>$null) -join ''
  if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($branch)) {
    $branch = (& git rev-parse --short HEAD 2>$null) -join ''
  }
  $branch = $branch.Trim()
  if ($branch -eq '') { return $empty }
  $upstream = Cailoxo-Upstream-Info $branch
  Cailoxo-Start-Fetch $branch
  if (-not $script:CAILOXO_GIT_STATUS) { return @{ branch = $branch; status = ''; dirty = $false; upstream = $upstream.upstream; upstream_icon = $upstream.upstream_icon; upstream_url = $upstream.upstream_url } }

  $counts = @{ ahead = 0; behind = 0; action = 0; conflicted = 0; untracked = 0; modified = 0; staged = 0; renamed = 0; deleted = 0; stashed = 0 }
  $status = & git status --porcelain=v1 2>$null
  foreach ($line in @($status)) {
    if ([string]::IsNullOrEmpty($line) -or $line.Length -lt 2) { continue }
    $code = $line.Substring(0, 2)
    if ($code -eq '??') { $counts.untracked++; continue }
    if (@('DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU') -contains $code) { $counts.conflicted++; continue }
    $x = $line.Substring(0, 1)
    $y = $line.Substring(1, 1)
    if ($x -eq 'R') { $counts.renamed++ }
    if ($x -eq 'D' -or $y -eq 'D') { $counts.deleted++ }
    if ($x -ne ' ' -and $x -ne 'R' -and $x -ne 'D') { $counts.staged++ }
    if ($y -ne ' ' -and $y -ne 'D') { $counts.modified++ }
  }

  $counts.ahead = Cailoxo-Count-Command { git rev-list --count '@{upstream}..HEAD' }
  $counts.behind = Cailoxo-Count-Command { git rev-list --count 'HEAD..@{upstream}' }
  $action = Cailoxo-Git-Action
  $stash = & git stash list 2>$null
  if ($LASTEXITCODE -eq 0) { $counts.stashed = @($stash | Where-Object { $_ -ne '' }).Count }
  $dirty = (($counts.conflicted + $counts.untracked + $counts.modified + $counts.staged + $counts.renamed + $counts.deleted) -gt 0)

  $items = @()
  foreach ($name in @('behind', 'ahead', 'stashed', 'action', 'conflicted', 'staged', 'modified', 'untracked', 'renamed', 'deleted')) {
    $item = Cailoxo-Status-Item $name $counts[$name] $action
    if ($item -ne '') { $items += $item }
  }
  @{ branch = $branch; status = ($items -join $script:CAILOXO_STATUS_SEPARATOR); dirty = $dirty; upstream = $upstream.upstream; upstream_icon = $upstream.upstream_icon; upstream_url = $upstream.upstream_url }
}

function Cailoxo-Render-Full {
  param([int]$LastStatus)
  $osIcon = Cailoxo-Os-Icon
  $git = Cailoxo-Git-Info
  $gitRoot = Cailoxo-Git-Root-Name
  $gitVars = @{ status = $git.status; branch_icon = $script:CAILOXO_BRANCH_ICON; upstream_icon = $git.upstream_icon; upstream = $git.upstream; upstream_url = $git.upstream_url; branch = $git.branch }
  $gitText = if ($git.branch -eq '') { '' } else { Cailoxo-Apply-Template $script:CAILOXO_GIT_TEMPLATE $gitVars }
  $osText = Cailoxo-Apply-Template $script:CAILOXO_OS_TEMPLATE @{ icon = $osIcon }

  $width = 80
  try { $width = [Console]::WindowWidth } catch {}
  if ($width -le 0) { $width = 80 }
  $fixed = (Cailoxo-Plain-Template $osText).Length + (Cailoxo-Plain-Template $gitText).Length + $script:CAILOXO_HOME_ICON.Length + $script:CAILOXO_FOLDER_ICON.Length + 10
  $budget = [Math]::Max(1, $width - $fixed)
  $path = Cailoxo-Format-Path (Cailoxo-Shorten-Path $budget) $gitRoot
  $homePath = Cailoxo-Home-Path
  $isHome = [string]::Equals((Cailoxo-Normalize-Path ((Get-Location).ProviderPath)), $homePath, [StringComparison]::OrdinalIgnoreCase)
  $pathText = Cailoxo-Path-Template @{ path = $path; home = $isHome; home_icon = $script:CAILOXO_HOME_ICON; folder_icon = $script:CAILOXO_FOLDER_ICON }

  $gitStyle = if ($git.dirty) { $script:CAILOXO_GIT_DIRTY_STYLE } else { $script:CAILOXO_GIT_CLEAN_STYLE }
  $pathSep = if ($gitText -eq '') { $script:CAILOXO_PATH_SEP_LAST } elseif ($git.dirty) { $script:CAILOXO_PATH_SEP_DIRTY } else { $script:CAILOXO_PATH_SEP_CLEAN }
  $gitSep = if ($git.dirty) { $script:CAILOXO_GIT_SEP_DIRTY } else { $script:CAILOXO_GIT_SEP_CLEAN }
  $first = (Cailoxo-Osc7) + $script:CAILOXO_OS_STYLE + (Cailoxo-Style-Template $osText) + $script:CAILOXO_RESET + $script:CAILOXO_OS_TAIL
  $first += $script:CAILOXO_PATH_STYLE + (Cailoxo-Path-Url-Start) + (Cailoxo-Style-Template $pathText) + (Cailoxo-Path-Url-End) + $script:CAILOXO_RESET + $pathSep
  if ($gitText -ne '') { $first += $gitStyle + (Cailoxo-Git-Url-Start $git.upstream_url) + (Cailoxo-Style-Template $gitText) + (Cailoxo-Git-Url-End $git.upstream_url) + $script:CAILOXO_RESET + $gitSep }

  $promptStyle = if ($LastStatus -eq 0) { $script:CAILOXO_PROMPT_OK_STYLE } else { $script:CAILOXO_PROMPT_ERROR_STYLE }
  $suffix = if ($script:CAILOXO_FINAL_SPACE) { ' ' } else { '' }
  $first + "`n" + $promptStyle + (Cailoxo-Style-Template $script:CAILOXO_PROMPT_TEMPLATE) + $script:CAILOXO_RESET + $suffix
}

function Cailoxo-Render-Transient {
  param([int]$LastStatus)
  $promptStyle = if ($LastStatus -eq 0) { $script:CAILOXO_TRANSIENT_OK_STYLE } else { $script:CAILOXO_TRANSIENT_ERROR_STYLE }
  $promptStyle + (Cailoxo-Style-Template $script:CAILOXO_TRANSIENT_TEMPLATE) + $script:CAILOXO_RESET
}

function prompt {
  $originalSuccess = $?
  $originalLastExitCode = $global:LASTEXITCODE
  $script:CAILOXO_RENDERING = $true
  Cailoxo-Poll-Fetch
  $lastStatus = if ($originalSuccess) { 0 } elseif ($null -ne $originalLastExitCode) { [int]$originalLastExitCode } else { 1 }
  if ($script:CAILOXO_PROMPT_TYPE -eq 'transient') {
    $script:CAILOXO_PROMPT_TYPE = 'primary'
    $output = Cailoxo-Render-Transient $lastStatus
  } else {
    $output = Cailoxo-Render-Full $lastStatus
  }
  try { Set-PSReadLineOption -ExtraPromptLineCount ((($output -split "`n").Count) - 1) } catch {}
  $script:CAILOXO_RENDERING = $false
  $script:CAILOXO_FETCH_READY = $true
  $global:LASTEXITCODE = $originalLastExitCode
  $output
}
"#;
    out.push_str(
        &body
            .replace("__CAILOXO_STYLE_BODY__", &style_body)
            .replace("__CAILOXO_PLAIN_BODY__", &plain_body),
    );

    if transient_enabled {
        out.push_str(
            r#"
function Set-CailoxoTransientPrompt {
  $script:CAILOXO_PROMPT_TYPE = 'transient'
  try { [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt() } catch {}
}

try {
  Set-PSReadLineKeyHandler -Key Enter -BriefDescription 'CailoxoEnterKeyHandler' -ScriptBlock {
    try {
      Cailoxo-Poll-Fetch -Repaint
      $parseErrors = $null
      [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$null, [ref]$null, [ref]$parseErrors, [ref]$null)
      if ($null -eq $parseErrors -or $parseErrors.Count -eq 0) { Set-CailoxoTransientPrompt }
    } finally {
      [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
    }
  }
} catch {}
"#,
        );
    }

    Ok(())
}

fn edge(
    glyph: Option<&str>,
    fg: Option<&String>,
    bg: Option<&String>,
    invert: bool,
) -> Result<String> {
    let Some(glyph) = glyph else {
        return Ok(String::new());
    };
    let mut out = String::new();
    let (fg, bg) = if invert { (bg, fg) } else { (fg, bg) };
    out.push_str(&color_escape(fg, false)?);
    out.push_str(&color_escape(bg, true)?);
    out.push_str(glyph);
    out.push_str(RESET);
    Ok(out)
}

fn ps_string(input: &str) -> String {
    format!("'{}'", input.replace('\'', "''"))
}

#[cfg(test)]
mod tests {
    use crate::config::Config;

    use super::*;

    #[test]
    fn generates_pwsh() {
        let config = Config::parse(include_str!("../../cailoxo.toml")).unwrap();
        let script = generate(&config).unwrap();
        assert!(script.contains("function Cailoxo-Git-Info"));
        assert!(script.contains("function Cailoxo-Style-Template"));
        assert!(script.contains("function Cailoxo-Format-Path"));
        assert!(script.contains("function Cailoxo-Path-Url-Start"));
        assert!(script.contains("function Cailoxo-Git-Url-Start"));
        assert!(script.contains("$script:CAILOXO_OSC7 = $true"));
        assert!(script.contains("function Cailoxo-Start-Fetch"));
        assert!(script.contains("function Cailoxo-Git-Action"));
        assert!(script.contains("'action' = '{{ action }}'"));
        assert!(script.contains("@('behind', 'ahead', 'stashed', 'action', 'conflicted'"));
        assert!(script.contains(
            "$dirty = (($counts.conflicted + $counts.untracked + $counts.modified + $counts.staged + $counts.renamed + $counts.deleted) -gt 0)"
        ));
        assert!(!script.contains("dirty = ($items.Count -gt 0)"));
        assert!(script.contains("Set-PSReadLineKeyHandler"));
        assert!(script.contains("function prompt"));
    }

    #[test]
    fn pwsh_no_git_span_omits_git_runtime() {
        let config = Config::parse(NO_GIT_CONFIG).unwrap();
        let script = generate(&config).unwrap();
        assert!(!script.contains("Cailoxo-Git-Info"));
        assert!(!script.contains("git status"));
        assert!(!script.contains("git rev-parse"));
        assert!(!script.contains("Cailoxo-Start-Fetch"));
    }

    #[test]
    fn pwsh_no_git_span_keeps_gitdir_probe_when_format_used() {
        let config = Config::parse(NO_GIT_GITDIR_CONFIG).unwrap();
        let script = generate(&config).unwrap();
        assert!(script.contains("function Cailoxo-Git-Root-Name"));
        assert!(script.contains("git rev-parse --show-toplevel"));
        assert!(!script.contains("Cailoxo-Git-Info"));
        assert!(!script.contains("git status"));
    }

    #[test]
    fn pwsh_omits_upstream_icon_map_when_unused() {
        let config = Config::parse(BRANCH_ONLY_GIT_CONFIG).unwrap();
        let script = generate(&config).unwrap();
        assert!(!script.contains("$script:CAILOXO_UPSTREAM_ICONS = @{"));
    }

    const NO_GIT_CONFIG: &str = r#"
version = 1
final_space = true

[[line]]
[[line.span]]
type = "os"
template = " {{ icon }} "
background = "7"
tail = ">"

[[line.span]]
type = "path"
template = " {{ path }} "
background = "4"
separator = ">"

[[line]]
[[line.span]]
type = "text"
template = ">"
success_foreground = "2"
error_foreground = "1"
"#;

    const BRANCH_ONLY_GIT_CONFIG: &str = r#"
version = 1

[[line]]
[[line.span]]
type = "os"
template = " {{ icon }} "
background = "7"
tail = ">"

[[line.span]]
type = "path"
template = " {{ path }} "
background = "4"
separator = ">"

[[line.span]]
type = "git"
template = " {{ branch }} "
background = "2"
separator = ">"

[line.span.settings]
fetch_upstream_icon = true

[[line]]
[[line.span]]
type = "text"
template = ">"
"#;

    const NO_GIT_GITDIR_CONFIG: &str = r#"
version = 1

[[line]]
[[line.span]]
type = "os"
template = " {{ icon }} "
background = "7"
tail = ">"

[[line.span]]
type = "path"
template = " {{ path }} "
background = "4"
separator = ">"

[line.span.settings]
gitdir_format = "<b>%s</b>"

[[line]]
[[line.span]]
type = "text"
template = ">"
"#;
}
