use serde::Serialize;
use std::collections::HashMap;
use std::collections::HashSet;

use crate::ArgType;
use crate::ExecCall;
use crate::arg_matcher::ArgMatcher;
use crate::arg_resolver::PositionalArg;
use crate::arg_resolver::resolve_observed_args_with_patterns;
use crate::error::Error;
use crate::error::Result;
use crate::opt::Opt;
use crate::opt::OptMeta;
use crate::valid_exec::MatchedFlag;
use crate::valid_exec::MatchedOpt;
use crate::valid_exec::ValidExec;

#[derive(Debug)]
pub struct ProgramSpec {
    pub program: String,
    pub system_path: Vec<String>,
    pub option_bundling: bool,
    pub combined_format: bool,
    pub allowed_options: HashMap<String, Opt>,
    pub arg_patterns: Vec<ArgMatcher>,
    forbidden: Option<String>,
    required_options: HashSet<String>,
    should_match: Vec<Vec<String>>,
    should_not_match: Vec<Vec<String>>,
}

impl ProgramSpec {
    pub fn new(
        program: String,
        system_path: Vec<String>,
        option_bundling: bool,
        combined_format: bool,
        allowed_options: HashMap<String, Opt>,
        arg_patterns: Vec<ArgMatcher>,
        forbidden: Option<String>,
        should_match: Vec<Vec<String>>,
        should_not_match: Vec<Vec<String>>,
    ) -> Self {
        let required_options = allowed_options
            .iter()
            .filter_map(|(name, opt)| {
                if opt.required {
                    Some(name.clone())
                } else {
                    None
                }
            })
            .collect();
        Self {
            program,
            system_path,
            option_bundling,
            combined_format,
            allowed_options,
            arg_patterns,
            forbidden,
            required_options,
            should_match,
            should_not_match,
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum MatchedExec {
    Match { exec: ValidExec },
    Forbidden { cause: Forbidden, reason: String },
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub enum Forbidden {
    Program {
        program: String,
        exec_call: ExecCall,
    },
    Arg {
        arg: String,
        exec_call: ExecCall,
    },
    Exec {
        exec: ValidExec,
    },
}

impl ProgramSpec {
    // TODO(mbolin): The idea is that there should be a set of rules defined for
    // a program and the args should be checked against the rules to determine
    // if the program should be allowed to run.
    pub fn check(&self, exec_call: &ExecCall) -> Result<MatchedExec> {
        let mut expecting_option_value: Option<(String, ArgType)> = None;
        let mut args = Vec::<PositionalArg>::new();
        let mut matched_flags = Vec::<MatchedFlag>::new();
        let mut matched_opts = Vec::<MatchedOpt>::new();

        for (index, arg) in exec_call.args.iter().enumerate() {
            if let Some(expected) = expecting_option_value {
                // If we are expecting an option value, then the next argument
                // should be the value for the option.
                // This had better not be another option!
                let (name, arg_type) = expected;
                if arg.starts_with("-") {
                    return Err(Error::OptionFollowedByOptionInsteadOfValue {
                        program: self.program.clone(),
                        option: name,
                        value: arg.clone(),
                    });
                }

                matched_opts.push(MatchedOpt::new(&name, arg, arg_type)?);
                expecting_option_value = None;
            } else if arg == "--" {
                return Err(Error::DoubleDashNotSupportedYet {
                    program: self.program.clone(),
                });
            } else if arg.starts_with("-") {
                match self.allowed_options.get(arg) {
                    Some(opt) => {
                        match &opt.meta {
                            OptMeta::Flag => {
                                matched_flags.push(MatchedFlag { name: arg.clone() });
                                // A flag does not expect an argument: continue.
                                continue;
                            }
                            OptMeta::Value(arg_type) => {
                                expecting_option_value = Some((arg.clone(), arg_type.clone()));
                                continue;
                            }
                        }
                    }
                    None => {
                        // It could be an --option=value style flag...
                    }
                }

                return Err(Error::UnknownOption {
                    program: self.program.clone(),
                    option: arg.clone(),
                });
            } else {
                args.push(PositionalArg {
                    index,
                    value: arg.clone(),
                });
            }
        }

        if let Some(expected) = expecting_option_value {
            let (name, _arg_type) = expected;
            return Err(Error::OptionMissingValue {
                program: self.program.clone(),
                option: name,
            });
        }

        let matched_args =
            resolve_observed_args_with_patterns(&self.program, args, &self.arg_patterns)?;

        // Verify all required options are present.
        let matched_opt_names: HashSet<String> = matched_opts
            .iter()
            .map(|opt| opt.name().to_string())
            .collect();
        if !matched_opt_names.is_superset(&self.required_options) {
            let mut options = self
                .required_options
                .difference(&matched_opt_names)
                .map(String::from)
                .collect::<Vec<_>>();
            options.sort();
            return Err(Error::MissingRequiredOptions {
                program: self.program.clone(),
                options,
            });
        }

        let exec = ValidExec {
            program: self.program.clone(),
            flags: matched_flags,
            opts: matched_opts,
            args: matched_args,
            system_path: self.system_path.clone(),
        };
        match &self.forbidden {
            Some(reason) => Ok(MatchedExec::Forbidden {
                cause: Forbidden::Exec { exec },
                reason: reason.clone(),
            }),
            None => Ok(MatchedExec::Match { exec }),
        }
    }

    pub fn verify_should_match_list(&self) -> Vec<PositiveExampleFailedCheck> {
        let mut violations = Vec::new();
        for good in &self.should_match {
            let exec_call = ExecCall {
                program: self.program.clone(),
                args: good.clone(),
            };
            match self.check(&exec_call) {
                Ok(_) => {}
                Err(error) => {
                    violations.push(PositiveExampleFailedCheck {
                        program: self.program.clone(),
                        args: good.clone(),
                        error,
                    });
                }
            }
        }
        violations
    }

    pub fn verify_should_not_match_list(&self) -> Vec<NegativeExamplePassedCheck> {
        let mut violations = Vec::new();
        for bad in &self.should_not_match {
            let exec_call = ExecCall {
                program: self.program.clone(),
                args: bad.clone(),
            };
            if self.check(&exec_call).is_ok() {
                violations.push(NegativeExamplePassedCheck {
                    program: self.program.clone(),
                    args: bad.clone(),
                });
            }
        }
        violations
    }
}

#[derive(Debug, Eq, PartialEq)]
pub struct PositiveExampleFailedCheck {
    pub program: String,
    pub args: Vec<String>,
    pub error: Error,
}

#[derive(Debug, Eq, PartialEq)]
pub struct NegativeExamplePassedCheck {
    pub program: String,
    pub args: Vec<String>,
}
