use crate::errors::{ProblemError, ProblemResult};
use crate::problem::{
    Constraint, ConstraintType, Objective, ObjectiveType, Problem, Variable, VariableType,
};
use std::collections::HashMap;

/// Builder pattern for constructing optimization problems
pub struct ProblemBuilder {
    variables: Vec<Variable>,
    variable_names: HashMap<String, usize>,
    constraints: Vec<Constraint>,
    objective: Option<Objective>,
    name: Option<String>,
}

impl Default for ProblemBuilder {
    fn default() -> Self {
        Self::new()
    }
}

impl ProblemBuilder {
    /// Creates a new problem builder
    pub fn new() -> Self {
        ProblemBuilder {
            variables: Vec::new(),
            variable_names: HashMap::new(),
            constraints: Vec::new(),
            objective: None,
            name: None,
        }
    }

    /// Sets the name of the problem
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Adds a continuous variable with given bounds
    pub fn add_variable(
        mut self,
        name: impl Into<String>,
        lower_bound: f64,
        upper_bound: f64,
    ) -> ProblemResult<Self> {
        let name = name.into();
        if self.variable_names.contains_key(&name) {
            return Err(ProblemError::DuplicateVariableName { name });
        }

        let variable = Variable::new_continuous(&name, lower_bound, upper_bound)?;
        let index = self.variables.len();
        self.variable_names.insert(name, index);
        self.variables.push(variable);
        Ok(self)
    }

    /// Adds a non-negative continuous variable
    pub fn add_non_negative_variable(mut self, name: impl Into<String>) -> ProblemResult<Self> {
        let name = name.into();
        if self.variable_names.contains_key(&name) {
            return Err(ProblemError::DuplicateVariableName { name: name.clone() });
        }

        let variable = Variable::new_non_negative(&name);
        let index = self.variables.len();
        self.variable_names.insert(name, index);
        self.variables.push(variable);
        Ok(self)
    }

    /// Adds a free (unbounded) continuous variable
    pub fn add_free_variable(mut self, name: impl Into<String>) -> ProblemResult<Self> {
        let name = name.into();
        if self.variable_names.contains_key(&name) {
            return Err(ProblemError::DuplicateVariableName { name: name.clone() });
        }

        let variable = Variable::new_free(&name);
        let index = self.variables.len();
        self.variable_names.insert(name, index);
        self.variables.push(variable);
        Ok(self)
    }

    /// Adds an integer variable with given bounds
    pub fn add_integer_variable(
        mut self,
        name: impl Into<String>,
        lower_bound: f64,
        upper_bound: f64,
    ) -> ProblemResult<Self> {
        let name = name.into();
        if self.variable_names.contains_key(&name) {
            return Err(ProblemError::DuplicateVariableName { name: name.clone() });
        }

        let variable = Variable::new_integer(&name, lower_bound, upper_bound)?;
        let index = self.variables.len();
        self.variable_names.insert(name, index);
        self.variables.push(variable);
        Ok(self)
    }

    /// Adds a binary variable
    pub fn add_binary_variable(mut self, name: impl Into<String>) -> ProblemResult<Self> {
        let name = name.into();
        if self.variable_names.contains_key(&name) {
            return Err(ProblemError::DuplicateVariableName { name: name.clone() });
        }

        let variable = Variable::new_binary(&name);
        let index = self.variables.len();
        self.variable_names.insert(name, index);
        self.variables.push(variable);
        Ok(self)
    }

    /// Adds multiple variables at once
    pub fn add_variables<I, S>(
        mut self,
        names: I,
        lower_bound: f64,
        upper_bound: f64,
    ) -> ProblemResult<Self>
    where
        I: IntoIterator<Item = S>,
        S: Into<String>,
    {
        for name in names {
            self = self.add_variable(name, lower_bound, upper_bound)?;
        }
        Ok(self)
    }

    /// Adds a constraint using variable names
    pub fn add_constraint_by_name(
        mut self,
        terms: &[(impl AsRef<str>, f64)],
        constraint_type: ConstraintType,
        rhs: f64,
    ) -> ProblemResult<Self> {
        let mut coefficients = vec![0.0; self.variables.len()];

        for (var_name, coeff) in terms {
            let var_name = var_name.as_ref();
            let &index = self.variable_names.get(var_name).ok_or_else(|| {
                ProblemError::InvalidConstraint {
                    reason: format!("Variable '{}' not found", var_name),
                }
            })?;
            coefficients[index] = *coeff;
        }

        let constraint = Constraint::new(coefficients, constraint_type, rhs)?;
        self.constraints.push(constraint);
        Ok(self)
    }

    /// Adds a constraint using variable indices
    pub fn add_constraint(
        mut self,
        coefficients: Vec<f64>,
        constraint_type: ConstraintType,
        rhs: f64,
    ) -> ProblemResult<Self> {
        if coefficients.len() != self.variables.len() {
            return Err(ProblemError::InvalidConstraintLength {
                expected: self.variables.len(),
                actual: coefficients.len(),
            });
        }

        let constraint = Constraint::new(coefficients, constraint_type, rhs)?;
        self.constraints.push(constraint);
        Ok(self)
    }

    /// Adds a named constraint
    pub fn add_named_constraint(
        mut self,
        name: impl Into<String>,
        coefficients: Vec<f64>,
        constraint_type: ConstraintType,
        rhs: f64,
    ) -> ProblemResult<Self> {
        if coefficients.len() != self.variables.len() {
            return Err(ProblemError::InvalidConstraintLength {
                expected: self.variables.len(),
                actual: coefficients.len(),
            });
        }

        let constraint = Constraint::new_with_name(name, coefficients, constraint_type, rhs)?;
        self.constraints.push(constraint);
        Ok(self)
    }

    /// Sets the objective function using variable names
    pub fn objective_by_name(
        mut self,
        terms: &[(impl AsRef<str>, f64)],
        objective_type: ObjectiveType,
    ) -> ProblemResult<Self> {
        let mut coefficients = vec![0.0; self.variables.len()];

        for (var_name, coeff) in terms {
            let var_name = var_name.as_ref();
            let &index = self.variable_names.get(var_name).ok_or_else(|| {
                ProblemError::InvalidObjective {
                    reason: format!("Variable '{}' not found", var_name),
                }
            })?;
            coefficients[index] = *coeff;
        }

        let objective = Objective::new(coefficients, objective_type)?;
        self.objective = Some(objective);
        Ok(self)
    }

    /// Sets the objective function using variable indices
    pub fn objective(
        mut self,
        coefficients: Vec<f64>,
        objective_type: ObjectiveType,
    ) -> ProblemResult<Self> {
        if coefficients.len() != self.variables.len() {
            return Err(ProblemError::DimensionMismatch {
                expected: self.variables.len(),
                actual: coefficients.len(),
            });
        }

        let objective = Objective::new(coefficients, objective_type)?;
        self.objective = Some(objective);
        Ok(self)
    }

    /// Sets a named objective function
    pub fn named_objective(
        mut self,
        name: impl Into<String>,
        coefficients: Vec<f64>,
        objective_type: ObjectiveType,
    ) -> ProblemResult<Self> {
        if coefficients.len() != self.variables.len() {
            return Err(ProblemError::DimensionMismatch {
                expected: self.variables.len(),
                actual: coefficients.len(),
            });
        }

        let objective = Objective::new_with_name(name, coefficients, objective_type)?;
        self.objective = Some(objective);
        Ok(self)
    }

    /// Builds the final problem
    pub fn build(self) -> ProblemResult<Problem> {
        let objective = self
            .objective
            .ok_or_else(|| ProblemError::InvalidObjective {
                reason: "No objective function specified".to_string(),
            })?;

        if self.variables.is_empty() {
            return Err(ProblemError::EmptyProblem);
        }

        let mut problem = Problem::new(self.variables, self.constraints, objective)?;
        if let Some(name) = self.name {
            problem.name = Some(name);
        }

        Ok(problem)
    }

    /// Gets the number of variables currently in the builder
    pub fn num_variables(&self) -> usize {
        self.variables.len()
    }

    /// Gets the number of constraints currently in the builder
    pub fn num_constraints(&self) -> usize {
        self.constraints.len()
    }

    /// Checks if a variable name exists
    pub fn has_variable(&self, name: &str) -> bool {
        self.variable_names.contains_key(name)
    }

    /// Gets variable index by name
    pub fn get_variable_index(&self, name: &str) -> Option<usize> {
        self.variable_names.get(name).copied()
    }
}

/// Convenience functions for common problem types
impl ProblemBuilder {
    /// Creates a standard linear programming problem
    pub fn linear_program() -> Self {
        Self::new()
    }

    /// Creates a mixed-integer programming problem
    pub fn mixed_integer_program() -> Self {
        Self::new()
    }

    /// Creates a transportation problem structure
    pub fn transportation() -> Self {
        Self::new()
    }

    /// Creates an assignment problem structure
    pub fn assignment() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builder_basic_usage() {
        let problem = ProblemBuilder::new()
            .name("Test Problem")
            .add_non_negative_variable("x")
            .unwrap()
            .add_non_negative_variable("y")
            .unwrap()
            .add_constraint_by_name(&[("x", 1.0), ("y", 1.0)], ConstraintType::LessEqual, 10.0)
            .unwrap()
            .objective_by_name(&[("x", 3.0), ("y", 2.0)], ObjectiveType::Maximize)
            .unwrap()
            .build()
            .unwrap();

        assert_eq!(problem.name, Some("Test Problem".to_string()));
        assert_eq!(problem.num_variables(), 2);
        assert_eq!(problem.num_constraints(), 1);
        assert_eq!(problem.objective.objective_type, ObjectiveType::Maximize);
    }

    #[test]
    fn test_builder_with_indices() {
        let problem = ProblemBuilder::new()
            .add_variable("x1", 0.0, f64::INFINITY)
            .unwrap()
            .add_variable("x2", 0.0, f64::INFINITY)
            .unwrap()
            .add_constraint(vec![2.0, 3.0], ConstraintType::LessEqual, 12.0)
            .unwrap()
            .objective(vec![5.0, 4.0], ObjectiveType::Maximize)
            .unwrap()
            .build()
            .unwrap();

        assert_eq!(problem.num_variables(), 2);
        assert_eq!(problem.constraints[0].coefficients, vec![2.0, 3.0]);
        assert_eq!(problem.objective.coefficients, vec![5.0, 4.0]);
    }

    #[test]
    fn test_builder_errors() {
        // Duplicate variable name
        let result = ProblemBuilder::new()
            .add_variable("x", 0.0, 10.0)
            .unwrap()
            .add_variable("x", 0.0, 20.0);
        assert!(result.is_err());

        // No objective
        let result = ProblemBuilder::new()
            .add_variable("x", 0.0, 10.0)
            .unwrap()
            .build();
        assert!(result.is_err());

        // No variables
        let result = ProblemBuilder::new().objective(vec![], ObjectiveType::Maximize);
        assert!(result.is_err());
    }

    #[test]
    fn test_builder_integer_and_binary() {
        let problem = ProblemBuilder::new()
            .add_integer_variable("i", 0.0, 100.0)
            .unwrap()
            .add_binary_variable("b")
            .unwrap()
            .add_constraint(vec![1.0, 10.0], ConstraintType::LessEqual, 50.0)
            .unwrap()
            .objective(vec![1.0, 5.0], ObjectiveType::Maximize)
            .unwrap()
            .build()
            .unwrap();

        assert_eq!(problem.variables[0].var_type, VariableType::Integer);
        assert_eq!(problem.variables[1].var_type, VariableType::Binary);

        let stats = problem.statistics();
        assert_eq!(stats.num_integer, 1);
        assert_eq!(stats.num_binary, 1);
        assert!(stats.is_integer);
    }

    #[test]
    fn test_builder_multiple_variables() {
        let problem = ProblemBuilder::new()
            .add_variables(["x1", "x2", "x3"], 0.0, f64::INFINITY)
            .unwrap()
            .add_constraint(vec![1.0, 1.0, 1.0], ConstraintType::Equal, 1.0)
            .unwrap()
            .objective(vec![2.0, 3.0, 1.0], ObjectiveType::Minimize)
            .unwrap()
            .build()
            .unwrap();

        assert_eq!(problem.num_variables(), 3);
        assert!(problem.find_variable("x2").is_some());
    }
}
