use std::collections::HashMap;

use crate::Location;
use crate::parser::TypeId;

use super::value::Value;

/// A binding in the environment.
#[derive(Clone, Debug)]
pub struct Binding {
    pub value: Value,
    pub type_id: Option<TypeId>,
    pub mutable: bool,
    pub is_const: bool,
    pub used: bool,
    pub loc: Option<Location>,
}

impl Binding {
    /// Create a new constant binding.
    pub fn constant(value: Value, type_id: TypeId) -> Self {
        Self {
            value,
            type_id: Some(type_id),
            mutable: false,
            is_const: true,
            used: false,
            loc: None,
        }
    }

    /// Create a new variable binding.
    pub fn variable(value: Value, type_id: Option<TypeId>, mutable: bool) -> Self {
        Self {
            value,
            type_id,
            mutable,
            is_const: false,
            used: false,
            loc: None,
        }
    }

    /// Attach a declaration location for unused-binding warnings.
    pub fn with_loc(mut self, loc: Location) -> Self {
        self.loc = Some(loc);
        self
    }
}

/// A scope with a unique identifier and a set of bindings.
#[derive(Debug)]
struct Scope {
    id: u64,
    bindings: HashMap<String, Binding>,
}

/// The environment stores variable and constant bindings using a scope stack.
#[derive(Debug)]
pub struct Environment {
    scopes: Vec<Scope>,
    scope_counter: u64,
}

impl Default for Environment {
    fn default() -> Self {
        Self {
            scopes: vec![Scope {
                id: 0,
                bindings: HashMap::new(),
            }],
            scope_counter: 0,
        }
    }
}

impl Environment {
    /// Create a new environment with a global scope.
    pub fn new() -> Self {
        Self::default()
    }

    /// Push a new scope onto the stack.
    pub fn push_scope(&mut self) {
        self.scope_counter += 1;
        self.scopes.push(Scope {
            id: self.scope_counter,
            bindings: HashMap::new(),
        });
    }

    /// Pop the current scope from the stack.
    pub fn pop_scope(&mut self) {
        if self.scopes.len() > 1 {
            self.scopes.pop();
        }
    }

    /// Find the scope ID that contains a binding for `name`, searching innermost-first.
    pub fn scope_id_of(&self, name: &str) -> Option<u64> {
        for scope in self.scopes.iter().rev() {
            if scope.bindings.contains_key(name) {
                return Some(scope.id);
            }
        }
        None
    }

    /// Check if a name is defined in the current (innermost) scope.
    pub fn contains(&self, name: &str) -> bool {
        self.scopes
            .last()
            .is_some_and(|scope| scope.bindings.contains_key(name))
    }

    /// Define a new binding in the current (innermost) scope.
    ///
    /// Returns the displaced binding if a binding for `name` already existed in the same scope.
    ///
    pub fn define(&mut self, name: String, binding: Binding) -> Option<Binding> {
        if let Some(scope) = self.scopes.last_mut() {
            scope.bindings.insert(name, binding)
        } else {
            None
        }
    }

    /// Get a binding by name, searching from innermost to outermost scope.
    pub fn get(&self, name: &str) -> Option<&Binding> {
        for scope in self.scopes.iter().rev() {
            if let Some(binding) = scope.bindings.get(name) {
                return Some(binding);
            }
        }

        None
    }

    /// Get a mutable binding by name, searching from innermost to outermost scope.
    pub fn get_mut(&mut self, name: &str) -> Option<&mut Binding> {
        for scope in self.scopes.iter_mut().rev() {
            if let Some(binding) = scope.bindings.get_mut(name) {
                return Some(binding);
            }
        }

        None
    }

    /// Get a binding in a specific scope by scope ID.
    pub fn get_in_scope(&self, name: &str, scope_id: u64) -> Option<&Binding> {
        for scope in self.scopes.iter().rev() {
            if scope.id == scope_id {
                return scope.bindings.get(name);
            }
        }

        None
    }

    /// Get a mutable binding in a specific scope by scope ID.
    pub fn get_mut_in_scope(&mut self, name: &str, scope_id: u64) -> Option<&mut Binding> {
        for scope in self.scopes.iter_mut().rev() {
            if scope.id == scope_id {
                return scope.bindings.get_mut(name);
            }
        }

        None
    }

    /// Iterate over the bottom (global) scope's bindings.
    pub fn iter_global(&self) -> impl Iterator<Item = (&String, &Binding)> {
        self.scopes[0].bindings.iter()
    }

    /// Define a binding directly in the global (bottom) scope.
    pub fn define_global(&mut self, name: String, binding: Binding) {
        if let Some(scope) = self.scopes.first_mut() {
            scope.bindings.insert(name, binding);
        }
    }

    /// Check if a name exists in the global (bottom) scope.
    pub fn contains_global(&self, name: &str) -> bool {
        self.scopes
            .first()
            .is_some_and(|s| s.bindings.contains_key(name))
    }

    /// Return all unused bindings in the current (innermost) scope.
    ///
    /// Excludes names starting with `_` and node type schemas.
    ///
    pub fn unused_in_current_scope(&self) -> Vec<(String, Option<Location>)> {
        let Some(scope) = self.scopes.last() else {
            return Vec::new();
        };

        scope
            .bindings
            .iter()
            .filter(|(name, binding)| {
                !binding.used
                    && !name.starts_with('_')
                    && !matches!(binding.value, Value::NodeType(_))
            })
            .map(|(name, binding)| (name.clone(), binding.loc))
            .collect()
    }
}
