use std::{
    fs,
    path::{Path, PathBuf},
    pin::Pin,
    sync::Arc,
};

use crate::{
    ast::{BoolOp, CommandNode, RedirectionKind, ShellExpr},
    shell::{ExitCode, SharedShellState, ShellResult, ShellState},
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Word {
    pub segments: Vec<WordSegment>,
}

impl Word {
    pub fn new(segments: Vec<WordSegment>) -> Self {
        Self { segments }
    }

    pub fn literal(text: impl Into<String>) -> Self {
        Self {
            segments: vec![WordSegment::Literal {
                text: text.into(),
                quote: QuoteKind::Unquoted,
            }],
        }
    }

    pub fn expand(&self, state: &ShellState) -> String {
        expand_word_sync(state, self)
    }

    pub fn split_assignment(&self) -> Option<(String, Word)> {
        let WordSegment::Literal {
            text,
            quote: QuoteKind::Unquoted,
        } = self.segments.first()?
        else {
            return None;
        };

        let equals = text.find('=')?;
        let name = &text[..equals];
        if !is_valid_assignment_name(name) {
            return None;
        }

        let mut value_segments = Vec::with_capacity(self.segments.len());
        let suffix = &text[equals + 1..];
        if !suffix.is_empty() {
            value_segments.push(WordSegment::Literal {
                text: suffix.to_string(),
                quote: QuoteKind::Unquoted,
            });
        }
        value_segments.extend(self.segments.iter().skip(1).cloned());

        Some((name.to_string(), Word::new(value_segments)))
    }

    pub fn quote_removed_text(&self) -> String {
        let mut out = String::new();

        for segment in &self.segments {
            match segment {
                WordSegment::Literal { text, .. } => out.push_str(text),
                WordSegment::Variable { name, .. } => {
                    out.push('$');
                    out.push_str(name);
                }
                WordSegment::LastStatus { .. } => out.push_str("$?"),
                WordSegment::CommandSubstitution { expr, .. } => {
                    out.push_str("$(");
                    out.push_str(&render_shell_expr(expr));
                    out.push(')');
                }
            }
        }

        out
    }

    pub fn is_quoted(&self) -> bool {
        self.segments
            .iter()
            .any(|segment| !matches!(segment.quote_kind(), QuoteKind::Unquoted))
    }

    pub fn as_unquoted_literal(&self) -> Option<&str> {
        match self.segments.as_slice() {
            [
                WordSegment::Literal {
                    text,
                    quote: QuoteKind::Unquoted,
                },
            ] => Some(text),
            _ => None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WordSegment {
    Literal {
        text: String,
        quote: QuoteKind,
    },
    Variable {
        name: String,
        quote: QuoteKind,
    },
    LastStatus {
        quote: QuoteKind,
    },
    CommandSubstitution {
        expr: Box<ShellExpr>,
        quote: QuoteKind,
    },
}

impl WordSegment {
    pub fn quote_kind(&self) -> QuoteKind {
        match self {
            WordSegment::Literal { quote, .. }
            | WordSegment::Variable { quote, .. }
            | WordSegment::LastStatus { quote }
            | WordSegment::CommandSubstitution { quote, .. } => *quote,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuoteKind {
    Unquoted,
    SingleQuoted,
    DoubleQuoted,
}

pub fn expand_words(state: &ShellState, words: &[Word]) -> Vec<String> {
    words
        .iter()
        .map(|word| expand_word_sync(state, word))
        .collect()
}

pub fn exit_code_to_string(code: ExitCode) -> String {
    code.as_u8().to_string()
}

fn expand_word_sync(state: &ShellState, word: &Word) -> String {
    let mut out = String::new();

    for (idx, segment) in word.segments.iter().enumerate() {
        match segment {
            WordSegment::Literal {
                text,
                quote: QuoteKind::Unquoted,
            } if idx == 0 => {
                if let Some(expanded) = expand_leading_tilde(text, state.env_var("HOME")) {
                    out.push_str(&expanded);
                } else {
                    out.push_str(text);
                }
            }
            WordSegment::Literal { text, .. } => out.push_str(text),
            WordSegment::Variable { name, quote } => {
                if matches!(quote, QuoteKind::SingleQuoted) {
                    out.push('$');
                    out.push_str(name);
                } else if let Some(value) = state.env_var(name) {
                    out.push_str(value);
                }
            }
            WordSegment::LastStatus { quote } => {
                if matches!(quote, QuoteKind::SingleQuoted) {
                    out.push_str("$?");
                } else {
                    out.push_str(&u8::from(state.last_exit_status()).to_string());
                }
            }
            WordSegment::CommandSubstitution { .. } => {}
        }
    }

    out
}

pub type CommandSubstitutionFuture =
    Pin<Box<dyn Future<Output = ShellResult<String>> + Send + 'static>>;

pub type CommandSubstitutionExecutor =
    Arc<dyn Fn(SharedShellState, ShellExpr) -> CommandSubstitutionFuture + Send + Sync>;

pub async fn expand_word_with_state(
    state: SharedShellState,
    word: &Word,
    substitution_executor: &CommandSubstitutionExecutor,
) -> ShellResult<String> {
    let mut out = String::new();

    for (idx, segment) in word.segments.iter().enumerate() {
        match segment {
            WordSegment::Literal {
                text,
                quote: QuoteKind::Unquoted,
            } if idx == 0 => {
                let home = {
                    let guard = state.read().await;
                    guard.env_var("HOME").map(ToOwned::to_owned)
                };

                if let Some(expanded) = expand_leading_tilde(text, home.as_deref()) {
                    out.push_str(&expanded);
                } else {
                    out.push_str(text);
                }
            }
            WordSegment::Literal { text, .. } => out.push_str(text),
            WordSegment::Variable { name, quote } => {
                if matches!(quote, QuoteKind::SingleQuoted) {
                    out.push('$');
                    out.push_str(name);
                } else {
                    let val = {
                        let guard = state.read().await;
                        guard.env_var(name).map(ToOwned::to_owned)
                    };

                    if let Some(val) = val {
                        out.push_str(&val);
                    }
                }
            }
            WordSegment::LastStatus { quote } => {
                if matches!(quote, QuoteKind::SingleQuoted) {
                    out.push_str("$?");
                } else {
                    let code = {
                        let guard = state.read().await;
                        guard.last_exit_status()
                    };

                    out.push_str(&exit_code_to_string(code));
                }
            }
            WordSegment::CommandSubstitution { expr, .. } => {
                let substituted = substitution_executor(state.clone(), (**expr).clone()).await?;
                out.push_str(&normalize_command_substitution_output(substituted));
            }
        }
    }

    Ok(out)
}

pub async fn expand_word_pathnames_with_state(
    state: SharedShellState,
    word: &Word,
    substitution_executor: &CommandSubstitutionExecutor,
) -> ShellResult<Vec<String>> {
    let expanded =
        expand_word_pattern_with_state(state.clone(), word, substitution_executor).await?;

    if !expanded.has_glob_meta {
        return Ok(vec![expanded.text]);
    }

    let cwd = state.read().await.cwd().to_path_buf();
    let matches = expand_glob_pattern(&cwd, &expanded.text, &expanded.components)?;

    if matches.is_empty() {
        Ok(vec![expanded.text])
    } else {
        Ok(matches)
    }
}

pub async fn expand_words_with_state(
    state: SharedShellState,
    words: &[Word],
    substitution_executor: &CommandSubstitutionExecutor,
) -> ShellResult<Vec<String>> {
    let mut out = Vec::with_capacity(words.len());

    for word in words {
        out.push(expand_word_with_state(state.clone(), word, substitution_executor).await?);
    }

    Ok(out)
}

pub async fn expand_words_pathnames_with_state(
    state: SharedShellState,
    words: &[Word],
    substitution_executor: &CommandSubstitutionExecutor,
) -> ShellResult<Vec<String>> {
    let mut out = Vec::new();

    for word in words {
        out.extend(
            expand_word_pathnames_with_state(state.clone(), word, substitution_executor).await?,
        );
    }

    Ok(out)
}

fn normalize_command_substitution_output(mut output: String) -> String {
    while output.ends_with('\n') {
        output.pop();
        if output.ends_with('\r') {
            output.pop();
        }
    }

    output
}

fn expand_leading_tilde(text: &str, home: Option<&str>) -> Option<String> {
    if text == "~" {
        return home.map(ToOwned::to_owned);
    }

    text.strip_prefix("~/")
        .and_then(|suffix| home.map(|home| format!("{home}/{suffix}")))
}

fn is_valid_assignment_name(name: &str) -> bool {
    let mut chars = name.chars();
    let Some(first) = chars.next() else {
        return false;
    };

    if !(first == '_' || first.is_ascii_alphabetic()) {
        return false;
    }

    chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}

fn render_shell_expr(expr: &ShellExpr) -> String {
    match expr {
        ShellExpr::Command(node) => render_command_node(node),
        ShellExpr::Pipeline(nodes) => nodes
            .iter()
            .map(render_command_node)
            .collect::<Vec<_>>()
            .join(" | "),
        ShellExpr::BooleanChain { first, rest } => {
            let mut out = render_shell_expr(first);
            for (op, expr) in rest {
                let op = match op {
                    BoolOp::And => "&&",
                    BoolOp::Or => "||",
                };
                out.push(' ');
                out.push_str(op);
                out.push(' ');
                out.push_str(&render_shell_expr(expr));
            }
            out
        }
        ShellExpr::Sequence(exprs) => exprs
            .iter()
            .map(render_shell_expr)
            .collect::<Vec<_>>()
            .join(" ; "),
    }
}

fn render_command_node(node: &CommandNode) -> String {
    match node {
        CommandNode::Simple(simple) => {
            let mut parts = simple
                .argv
                .iter()
                .map(Word::quote_removed_text)
                .collect::<Vec<_>>();

            parts.extend(simple.redirections.iter().map(|redirection| {
                let fd = redirection.fd.map(|fd| fd.to_string()).unwrap_or_default();
                let op = match &redirection.kind {
                    RedirectionKind::Input => "<",
                    RedirectionKind::OutputTruncate => ">",
                    RedirectionKind::OutputAppend => ">>",
                    RedirectionKind::HereDoc { .. } => "<<",
                };

                format!("{fd}{op}{}", redirection.target.quote_removed_text())
            }));

            parts.join(" ")
        }
        CommandNode::FunctionDef { name, body } => {
            format!("{name}() {{ {} }}", render_shell_expr(body))
        }
        CommandNode::Subshell(expr) => format!("({})", render_shell_expr(expr)),
    }
}

#[derive(Debug)]
struct ExpandedGlobWord {
    text: String,
    components: Vec<GlobComponent>,
    has_glob_meta: bool,
}

#[derive(Debug, Clone)]
struct GlobComponent {
    tokens: Vec<GlobToken>,
}

#[derive(Debug, Clone)]
enum GlobToken {
    Literal(char),
    AnyString,
    AnyChar,
    CharClass(CharClass),
}

#[derive(Debug, Clone)]
struct CharClass {
    negated: bool,
    items: Vec<CharClassItem>,
}

#[derive(Debug, Clone)]
enum CharClassItem {
    Char(char),
    Range(char, char),
}

async fn expand_word_pattern_with_state(
    state: SharedShellState,
    word: &Word,
    substitution_executor: &CommandSubstitutionExecutor,
) -> ShellResult<ExpandedGlobWord> {
    let mut text = String::new();
    let mut tokens = Vec::new();
    let mut has_glob_meta = false;

    for (idx, segment) in word.segments.iter().enumerate() {
        let quote = segment.quote_kind();
        let segment_text = match segment {
            WordSegment::Literal {
                text,
                quote: QuoteKind::Unquoted,
            } if idx == 0 => {
                let home = {
                    let guard = state.read().await;
                    guard.env_var("HOME").map(ToOwned::to_owned)
                };

                expand_leading_tilde(text, home.as_deref()).unwrap_or_else(|| text.clone())
            }
            WordSegment::Literal { text, .. } => text.clone(),
            WordSegment::Variable { name, .. } => {
                let guard = state.read().await;
                guard
                    .env_var(name)
                    .map(ToOwned::to_owned)
                    .unwrap_or_default()
            }
            WordSegment::LastStatus { .. } => {
                let guard = state.read().await;
                exit_code_to_string(guard.last_exit_status())
            }
            WordSegment::CommandSubstitution { expr, .. } => {
                let substituted = substitution_executor(state.clone(), (**expr).clone()).await?;
                normalize_command_substitution_output(substituted)
            }
        };

        text.push_str(&segment_text);

        if matches!(quote, QuoteKind::Unquoted) {
            let parsed = parse_glob_tokens(&segment_text);
            has_glob_meta |= parsed.1;
            tokens.extend(parsed.0);
        } else {
            tokens.extend(segment_text.chars().map(GlobToken::Literal));
        }
    }

    Ok(ExpandedGlobWord {
        text,
        components: split_glob_components(tokens),
        has_glob_meta,
    })
}

fn parse_glob_tokens(text: &str) -> (Vec<GlobToken>, bool) {
    let chars = text.chars().collect::<Vec<_>>();
    let mut tokens = Vec::new();
    let mut idx = 0usize;
    let mut has_glob_meta = false;

    while idx < chars.len() {
        match chars[idx] {
            '*' => {
                tokens.push(GlobToken::AnyString);
                has_glob_meta = true;
                idx += 1;
            }
            '?' => {
                tokens.push(GlobToken::AnyChar);
                has_glob_meta = true;
                idx += 1;
            }
            '[' => {
                if let Some((class, consumed)) = parse_char_class(&chars[idx..]) {
                    tokens.push(GlobToken::CharClass(class));
                    has_glob_meta = true;
                    idx += consumed;
                } else {
                    tokens.push(GlobToken::Literal('['));
                    idx += 1;
                }
            }
            ch => {
                tokens.push(GlobToken::Literal(ch));
                idx += 1;
            }
        }
    }

    (tokens, has_glob_meta)
}

fn parse_char_class(chars: &[char]) -> Option<(CharClass, usize)> {
    if chars.len() < 2 || chars[0] != '[' {
        return None;
    }

    let mut idx = 1usize;
    let mut negated = false;
    if matches!(chars.get(idx), Some('!' | '^')) {
        negated = true;
        idx += 1;
    }

    let mut items = Vec::new();
    let mut closed = false;

    while idx < chars.len() {
        if chars[idx] == ']' && !items.is_empty() {
            idx += 1;
            closed = true;
            break;
        }

        let start = chars[idx];
        if idx + 2 < chars.len() && chars[idx + 1] == '-' && chars[idx + 2] != ']' {
            items.push(CharClassItem::Range(start, chars[idx + 2]));
            idx += 3;
        } else {
            items.push(CharClassItem::Char(start));
            idx += 1;
        }
    }

    closed.then_some((CharClass { negated, items }, idx))
}

fn split_glob_components(tokens: Vec<GlobToken>) -> Vec<GlobComponent> {
    let mut components = Vec::new();
    let mut current = Vec::new();

    for token in tokens {
        if matches!(token, GlobToken::Literal('/')) {
            components.push(GlobComponent { tokens: current });
            current = Vec::new();
        } else {
            current.push(token);
        }
    }

    components.push(GlobComponent { tokens: current });
    components
}

fn expand_glob_pattern(
    cwd: &Path,
    text: &str,
    components: &[GlobComponent],
) -> ShellResult<Vec<String>> {
    let absolute = text.starts_with('/');
    let mut matches = Vec::new();
    let start = if absolute {
        PathBuf::from("/")
    } else {
        cwd.to_path_buf()
    };
    let prefix = if absolute {
        String::from("/")
    } else {
        String::new()
    };

    expand_glob_component(&start, prefix, components, 0, &mut matches)?;
    matches.sort();
    Ok(matches)
}

fn expand_glob_component(
    current_dir: &Path,
    display_prefix: String,
    components: &[GlobComponent],
    idx: usize,
    matches: &mut Vec<String>,
) -> ShellResult<()> {
    if idx >= components.len() {
        matches.push(if display_prefix.is_empty() {
            ".".to_string()
        } else {
            display_prefix
        });
        return Ok(());
    }

    let component = &components[idx];
    if component.tokens.is_empty() {
        let next_dir = if current_dir == Path::new("/") {
            PathBuf::from("/")
        } else {
            current_dir.to_path_buf()
        };
        return expand_glob_component(&next_dir, display_prefix, components, idx + 1, matches);
    }

    if !component_has_glob(component) {
        let name = component_literal_text(component);
        let next_dir = current_dir.join(&name);
        let next_display = join_display_path(&display_prefix, &name);
        return expand_glob_component(&next_dir, next_display, components, idx + 1, matches);
    }

    let mut entries = fs::read_dir(current_dir)?.collect::<Result<Vec<_>, _>>()?;
    entries.sort_by_key(|entry| entry.file_name());

    for entry in entries {
        let name = entry.file_name().to_string_lossy().into_owned();
        if !component_matches_hidden_rule(component, &name) {
            continue;
        }
        if !glob_component_matches(component, &name) {
            continue;
        }

        let path = entry.path();
        if idx + 1 < components.len() && !path.is_dir() {
            continue;
        }

        let next_display = join_display_path(&display_prefix, &name);
        expand_glob_component(&path, next_display, components, idx + 1, matches)?;
    }

    Ok(())
}

fn component_has_glob(component: &GlobComponent) -> bool {
    component.tokens.iter().any(|token| {
        matches!(
            token,
            GlobToken::AnyString | GlobToken::AnyChar | GlobToken::CharClass(_)
        )
    })
}

fn component_literal_text(component: &GlobComponent) -> String {
    component
        .tokens
        .iter()
        .filter_map(|token| match token {
            GlobToken::Literal(ch) => Some(*ch),
            _ => None,
        })
        .collect()
}

fn join_display_path(prefix: &str, name: &str) -> String {
    if prefix.is_empty() || prefix == "/" {
        format!("{prefix}{name}")
    } else {
        format!("{prefix}/{name}")
    }
}

fn component_matches_hidden_rule(component: &GlobComponent, name: &str) -> bool {
    if !name.starts_with('.') {
        return true;
    }

    matches!(component.tokens.first(), Some(GlobToken::Literal('.')))
}

fn glob_component_matches(component: &GlobComponent, text: &str) -> bool {
    let chars = text.chars().collect::<Vec<_>>();
    glob_tokens_match(&component.tokens, &chars, 0, 0)
}

fn glob_tokens_match(
    tokens: &[GlobToken],
    text: &[char],
    token_idx: usize,
    text_idx: usize,
) -> bool {
    if token_idx == tokens.len() {
        return text_idx == text.len();
    }

    match &tokens[token_idx] {
        GlobToken::Literal(ch) => {
            text.get(text_idx) == Some(ch)
                && glob_tokens_match(tokens, text, token_idx + 1, text_idx + 1)
        }
        GlobToken::AnyChar => {
            text.get(text_idx).is_some()
                && glob_tokens_match(tokens, text, token_idx + 1, text_idx + 1)
        }
        GlobToken::AnyString => (text_idx..=text.len())
            .any(|next_idx| glob_tokens_match(tokens, text, token_idx + 1, next_idx)),
        GlobToken::CharClass(class) => {
            text.get(text_idx)
                .is_some_and(|ch| char_class_matches(class, *ch))
                && glob_tokens_match(tokens, text, token_idx + 1, text_idx + 1)
        }
    }
}

fn char_class_matches(class: &CharClass, ch: char) -> bool {
    let matched = class.items.iter().any(|item| match item {
        CharClassItem::Char(item) => *item == ch,
        CharClassItem::Range(start, end) => *start <= ch && ch <= *end,
    });

    if class.negated { !matched } else { matched }
}
