use crate::configuration::{KeywordFormatter, KeywordPreprocessor};
use crate::node::{RefinedArgumentsAtom, RefinedArgumentsNode};
use crate::utils::builtin_schemas;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::{PyString, PyTuple};
use pyo3::{FromPyObject, PyAny};
use std::cmp::min;
use std::collections::HashMap;
use std::sync::LazyLock;

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct KeywordMatcher {
    pub first: String,
    pub second: Option<String>,
}

fn single_word_matcher(s: &str) -> KeywordMatcher {
    KeywordMatcher {
        first: s.to_string(),
        second: None,
    }
}

pub fn single_word_matchers(s: Vec<String>) -> Vec<KeywordMatcher> {
    s.into_iter().map(|x| single_word_matcher(&x)).collect()
}

#[derive(Clone, Debug, FromPyObject)]
pub struct ArgumentSchema {
    pub options: Vec<KeywordMatcher>,
    pub one_value_keywords: Vec<KeywordMatcher>,
    pub multi_value_keywords: Vec<KeywordMatcher>,
    pub front_positional_arguments: Vec<String>,
    pub back_positional_arguments: Vec<String>,

    #[pyo3(default)]
    pub sections: HashMap<KeywordMatcher, ArgumentSchema>,

    pub keyword_preprocessors: HashMap<String, KeywordPreprocessor>,
    pub keyword_formatters: HashMap<String, KeywordFormatter>,
}

impl FromPyObject<'_, '_> for KeywordMatcher {
    type Error = PyErr;

    fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<Self, Self::Error> {
        let (first, second) = if obj.is_instance_of::<PyString>() {
            (obj.cast::<PyString>()?.str()?.to_string(), None)
        } else if obj.is_instance_of::<PyTuple>() {
            let (left, right) = (obj.get_item(0)?, obj.get_item(1)?);

            let left = left.cast::<PyString>()?.str()?.to_string();
            let right = if right.is_instance_of::<PyString>() {
                Some(right.cast::<PyString>()?.str()?.to_string())
            } else {
                None
            };

            (left, right)
        } else {
            return Err(PyRuntimeError::new_err("Invalid keyword matcher"));
        };

        Ok(KeywordMatcher { first, second })
    }
}

enum AccumulatorKind {
    Nodes,
    PositionalArguments,
}

struct Accumulator<'a> {
    kind: AccumulatorKind,
    nodes: RefinedArgumentsNode<'a>,
}

struct KeywordSplitter<'a> {
    groups: RefinedArgumentsNode<'a>,
    accumulator: Accumulator<'a>,
    comment_accumulator: RefinedArgumentsNode<'a>,
}

impl<'a> KeywordSplitter<'a> {
    fn flush_accumulators(&mut self) {
        let mut nodes = std::mem::take(&mut self.accumulator.nodes);
        if !nodes.is_empty() {
            self.groups.push(match self.accumulator.kind {
                AccumulatorKind::PositionalArguments => {
                    RefinedArgumentsAtom::PositionalArguments(nodes)
                }
                AccumulatorKind::Nodes => {
                    let arguments = nodes.split_off(1);
                    RefinedArgumentsAtom::MultiValueArgument {
                        keyword: Box::new(nodes.pop().unwrap()),
                        arguments,
                    }
                }
            });
        }
        self.groups.append(&mut self.comment_accumulator);
    }

    fn split(&mut self, schema: &ArgumentSchema, arguments: RefinedArgumentsNode<'a>) {
        let mut iterator = arguments.into_iter();

        while let Some(argument) = iterator.next() {
            if argument.is_comment() {
                self.comment_accumulator.push(argument);
            } else {
                let value = argument.get_keyword_value();
                let value = value.as_ref();
                if is_one_of_keywords(value, &schema.options) {
                    self.flush_accumulators();
                    self.groups.push(RefinedArgumentsAtom::OptionArgument {
                        keyword: Box::new(argument),
                    });
                } else if is_one_of_keywords(value, &schema.one_value_keywords) {
                    self.flush_accumulators();
                    let mut arguments = vec![];
                    for n in iterator.by_ref() {
                        let stop = !n.is_comment();
                        arguments.push(n);
                        if stop {
                            break;
                        }
                    }
                    self.groups.push(RefinedArgumentsAtom::OneValueArgument {
                        keyword: Box::new(argument),
                        arguments,
                    });
                } else if argument.is_inline_hint()
                    || is_one_of_keywords(value, &schema.multi_value_keywords)
                {
                    self.flush_accumulators();
                    self.accumulator.kind = AccumulatorKind::Nodes;
                    self.accumulator.nodes = vec![argument];
                } else if !self.accumulator.nodes.is_empty() {
                    self.accumulator.nodes.append(&mut self.comment_accumulator);
                    self.accumulator.nodes.push(argument);
                } else {
                    if !matches!(self.accumulator.kind, AccumulatorKind::PositionalArguments) {
                        self.accumulator.kind = AccumulatorKind::PositionalArguments;
                    }
                    self.accumulator.nodes.append(&mut self.comment_accumulator);
                    self.accumulator.nodes.push(argument);
                }
            }
        }

        self.flush_accumulators();
    }
}

impl ArgumentSchema {
    fn is_one_of_keywords(&self, node: &RefinedArgumentsAtom) -> bool {
        if node.is_inline_hint() {
            return true;
        }
        let value = node.get_keyword_value();
        let value = value.as_ref();
        is_one_of_keywords(value, &self.options)
            || is_one_of_keywords(value, &self.one_value_keywords)
            || is_one_of_keywords(value, &self.multi_value_keywords)
    }

    fn find_pivot(&self, arguments: &[RefinedArgumentsAtom]) -> Option<usize> {
        for (index, argument) in arguments.iter().enumerate() {
            if self.is_one_of_keywords(argument) {
                return Some(index);
            }
        }
        None
    }

    fn separate_front<'a>(
        &self,
        mut arguments: RefinedArgumentsNode<'a>,
    ) -> (RefinedArgumentsNode<'a>, RefinedArgumentsNode<'a>) {
        match self.find_pivot(&arguments) {
            None => (arguments, vec![]),
            Some(pivot) => {
                let tail = arguments.split_off(pivot);
                (arguments, tail)
            }
        }
    }

    fn split_positional_arguments<'a>(
        &self,
        mut arguments: RefinedArgumentsNode<'a>,
    ) -> RefinedArgumentsNode<'a> {
        let last_index = min(arguments.len(), self.front_positional_arguments.len());
        let rest = arguments.split_off(last_index);
        let mut arguments = arguments
            .into_iter()
            .map(|argument| RefinedArgumentsAtom::PositionalArguments(vec![argument]))
            .collect::<Vec<_>>();

        if !rest.is_empty() {
            arguments.push(RefinedArgumentsAtom::PositionalArguments(rest));
        }

        arguments
    }

    fn split_by_keywords<'a>(
        &self,
        arguments: RefinedArgumentsNode<'a>,
    ) -> RefinedArgumentsNode<'a> {
        let mut keyword_splitter = KeywordSplitter {
            groups: vec![],
            accumulator: Accumulator {
                kind: AccumulatorKind::Nodes,
                nodes: vec![],
            },
            comment_accumulator: vec![],
        };
        keyword_splitter.split(self, arguments);
        keyword_splitter.groups
    }

    fn split_arguments<'a>(
        &self,
        mut arguments: RefinedArgumentsNode<'a>,
    ) -> (
        RefinedArgumentsNode<'a>,
        RefinedArgumentsNode<'a>,
        RefinedArgumentsNode<'a>,
    ) {
        let back = if self.back_positional_arguments.len() > arguments.len() {
            vec![]
        } else {
            let index = arguments.len() - self.back_positional_arguments.len();
            arguments.split_off(index)
        };
        let (front, tail) = self.separate_front(arguments);
        let front = self.split_positional_arguments(front);
        let keyworded_arguments = self.split_by_keywords(tail);

        (front, keyworded_arguments, back)
    }

    fn get_section_schema(&self, argument: &RefinedArgumentsAtom) -> Option<&ArgumentSchema> {
        let value = argument.get_keyword_value();
        let value = value.as_ref();
        for (item, schema) in &self.sections {
            if is_one_of_keywords(value, std::slice::from_ref(item)) {
                return Some(schema);
            }
        }
        None
    }

    fn split_multi_value_argument<'a>(
        &self,
        keyword: RefinedArgumentsAtom<'a>,
        arguments: Vec<RefinedArgumentsAtom<'a>>,
    ) -> RefinedArgumentsAtom<'a> {
        let Some(section_schema) = self.get_section_schema(&keyword) else {
            return RefinedArgumentsAtom::MultiValueArgument {
                keyword: Box::new(keyword),
                arguments,
            };
        };

        let rest = section_schema.split_arguments_with_sections(arguments);
        let mut values = Vec::<RefinedArgumentsAtom>::with_capacity(rest.len());
        for argument in rest {
            match argument {
                RefinedArgumentsAtom::PositionalArguments(mut arguments) => {
                    values.append(&mut arguments);
                }
                _ => values.push(argument),
            }
        }

        RefinedArgumentsAtom::Section {
            header: Box::new(keyword),
            values,
        }
    }

    fn fix_back_positional_arguments(&self, section_values: &mut Vec<RefinedArgumentsAtom>) {
        let pivot = self.back_positional_arguments.len();
        if pivot == 0 {
            return;
        }

        let last = section_values.last_mut();
        let Some(atom) = last else {
            return;
        };

        match atom {
            RefinedArgumentsAtom::OneValueArgument { arguments, .. }
            | RefinedArgumentsAtom::MultiValueArgument { arguments, .. } => {
                let last_rest = arguments.split_off(1);

                let mut left_in_place = last_rest;
                let back_positional_arguments =
                    left_in_place.split_off(left_in_place.len() - pivot);
                arguments.append(&mut left_in_place);

                section_values.push(RefinedArgumentsAtom::PositionalArguments(
                    back_positional_arguments,
                ));
            }
            _ => (),
        }
    }

    fn form_sections<'a, Arguments>(
        &self,
        arguments: Arguments,
        size_hint: usize,
    ) -> RefinedArgumentsNode<'a>
    where
        Arguments: IntoIterator<Item = RefinedArgumentsAtom<'a>>,
    {
        let mut result = RefinedArgumentsNode::with_capacity(size_hint);
        let mut section_schema: Option<&ArgumentSchema> = None;

        for argument in arguments {
            if section_schema.is_some_and(|x| x.is_one_of_keywords(&argument)) {
                let last = result.last_mut();
                if let Some(RefinedArgumentsAtom::Section { values, .. }) = last {
                    values.push(argument);
                }
            } else {
                if let Some(section_schema) = section_schema {
                    if let Some(RefinedArgumentsAtom::Section { ref mut values, .. }) =
                        result.last_mut()
                    {
                        section_schema.fix_back_positional_arguments(values);
                    }
                }

                section_schema = self.get_section_schema(&argument);
                result.push(argument);
            }
        }

        result
    }

    pub fn split_arguments_with_sections<'a>(
        &self,
        arguments: RefinedArgumentsNode<'a>,
    ) -> RefinedArgumentsNode<'a> {
        let size_hint = arguments.len();
        let (front, keyworded_arguments, back) = self.split_arguments(arguments);
        let preprocessed = front
            .into_iter()
            .chain(keyworded_arguments)
            .chain(back)
            .map(|argument| match argument {
                RefinedArgumentsAtom::MultiValueArgument { keyword, arguments } => {
                    self.split_multi_value_argument(*keyword, arguments)
                }
                _ => argument,
            });
        self.form_sections(preprocessed, size_hint)
    }
}

pub struct KeywordValue<'a> {
    first: &'a str,
    second: Option<&'a str>,
}

impl KeywordMatcher {
    fn match_value(&self, value: &KeywordValue<'_>) -> bool {
        let KeywordValue { first, second } = value;

        self.first == *first
            && match (&self.second, &second) {
                (Some(matcher), Some(value)) => matcher == *value,
                (Some(_), _) => false,
                (None, _) => true,
            }
    }
}

pub fn is_one_of_keywords(
    keyword_value: Option<&KeywordValue>,
    matchers: &[KeywordMatcher],
) -> bool {
    let Some(keyword_value) = keyword_value else {
        return false;
    };

    matchers.iter().any(|m| m.match_value(keyword_value))
}

impl RefinedArgumentsAtom<'_> {
    pub fn is_comment(&self) -> bool {
        match self {
            Self::Atom(atom) => atom.is_comment(),
            Self::BinaryOperation { .. }
            | Self::UnaryOperation { .. }
            | Self::OptionArgument { .. }
            | Self::OneValueArgument { .. }
            | Self::MultiValueArgument { .. }
            | Self::PositionalArguments(_)
            | Self::Section { .. }
            | Self::KeywordArgument { .. }
            | Self::Pair { .. } => false,
        }
    }

    pub fn get_keyword_value(&self) -> Option<KeywordValue<'_>> {
        match self {
            Self::Atom(atom) => atom.get_value().map(|value| KeywordValue {
                first: value,
                second: None,
            }),
            Self::KeywordArgument { first, second, .. } => Some(KeywordValue {
                first: first.get_value()?,
                second: second.get_value(),
            }),
            Self::OptionArgument { keyword }
            | Self::OneValueArgument { keyword, .. }
            | Self::MultiValueArgument { keyword, .. }
            | Self::Section {
                header: keyword, ..
            }
            | Self::Pair { first: keyword, .. } => keyword.get_keyword_value(),
            Self::BinaryOperation { .. }
            | Self::UnaryOperation { .. }
            | Self::PositionalArguments(_) => None,
        }
    }
}

fn isolate_unary_operators<'a>(
    operators: &[KeywordMatcher],
    arguments: RefinedArgumentsNode<'a>,
) -> RefinedArgumentsNode<'a> {
    let mut one_behind: Option<RefinedArgumentsAtom> = None;
    let mut result = RefinedArgumentsNode::with_capacity(arguments.len());
    for current in arguments {
        match one_behind {
            None => {
                one_behind = Some(current);
            }
            Some(one_behind_node) => {
                if is_one_of_keywords(one_behind_node.get_keyword_value().as_ref(), operators) {
                    if current.is_comment() {
                        result.push(RefinedArgumentsAtom::UnaryOperation {
                            operation: Box::new(one_behind_node),
                            operand: None,
                        });
                        result.push(current);
                    } else {
                        result.push(RefinedArgumentsAtom::UnaryOperation {
                            operation: Box::new(one_behind_node),
                            operand: Some(Box::new(current.clone())),
                        });
                    }
                    one_behind = None;
                } else {
                    result.push(one_behind_node);
                    one_behind = Some(current);
                }
            }
        }
    }

    if let Some(node) = one_behind {
        result.push(node);
    }

    result
}

fn isolate_binary_tests<'a>(
    operators: &[KeywordMatcher],
    arguments: RefinedArgumentsNode<'a>,
) -> RefinedArgumentsNode<'a> {
    let mut two_behind: Option<RefinedArgumentsAtom> = None;
    let mut one_behind: Option<RefinedArgumentsAtom> = None;
    let mut result = RefinedArgumentsNode::with_capacity(arguments.len());

    for current in arguments {
        match (two_behind, one_behind) {
            (None, one_behind_node) => {
                two_behind = one_behind_node;
                one_behind = Some(current);
            }
            (Some(two_behind_node), Some(one_behind_node)) => {
                if is_one_of_keywords(one_behind_node.get_keyword_value().as_ref(), operators) {
                    result.push(RefinedArgumentsAtom::BinaryOperation {
                        lhs: Box::new(two_behind_node),
                        operation: Box::new(one_behind_node),
                        rhs: Box::new(current),
                    });
                    two_behind = None;
                    one_behind = None;
                } else {
                    result.push(two_behind_node);
                    two_behind = Some(one_behind_node);
                    one_behind = Some(current);
                }
            }
            (Some(two_behind_node), None) => {
                result.push(two_behind_node);
                two_behind = None;
                one_behind = Some(current);
            }
        }
    }

    if let Some(node) = two_behind {
        result.push(node);
    }

    if let Some(node) = one_behind {
        result.push(node);
    }

    result
}

pub fn isolate_conditions(arguments: RefinedArgumentsNode) -> RefinedArgumentsNode {
    static UNARY_OPERATORS: LazyLock<[KeywordMatcher; 13]> = LazyLock::new(|| {
        [
            "COMMAND",
            "POLICY",
            "TARGET",
            "TEST",
            "EXISTS",
            "IS_DIRECTORY",
            "IS_SYMLINK",
            "IS_ABSOLUTE",
            "DEFINED",
            "IS_READABLE",
            "IS_WRITABLE",
            "IS_EXECUTABLE",
            "DIAGNOSTIC",
        ]
        .map(single_word_matcher)
    });
    static BINARY_OPERATORS: LazyLock<[KeywordMatcher; 19]> = LazyLock::new(|| {
        [
            "IS_NEWER_THAN",
            "MATCHES",
            "LESS",
            "GREATER",
            "EQUAL",
            "LESS_EQUAL",
            "GREATER_EQUAL",
            "STRLESS",
            "STRGREATER",
            "STREQUAL",
            "STRLESS_EQUAL",
            "STRGREATER_EQUAL",
            "VERSION_LESS",
            "VERSION_GREATER",
            "VERSION_EQUAL",
            "VERSION_LESS_EQUAL",
            "VERSION_GREATER_EQUAL",
            "IN_LIST",
            "PATH_EQUAL",
        ]
        .map(single_word_matcher)
    });
    static NOT_OPERATOR: LazyLock<[KeywordMatcher; 1]> =
        LazyLock::new(|| [single_word_matcher("NOT")]);
    static AND_OPERATOR: LazyLock<[KeywordMatcher; 1]> =
        LazyLock::new(|| [single_word_matcher("AND")]);
    static OR_OPERATOR: LazyLock<[KeywordMatcher; 1]> =
        LazyLock::new(|| [single_word_matcher("OR")]);

    isolate_unary_operators(
        OR_OPERATOR.as_ref(),
        isolate_unary_operators(
            AND_OPERATOR.as_ref(),
            isolate_unary_operators(
                NOT_OPERATOR.as_ref(),
                isolate_binary_tests(
                    BINARY_OPERATORS.as_ref(),
                    isolate_unary_operators(UNARY_OPERATORS.as_ref(), arguments),
                ),
            ),
        ),
    )
}

pub type Signatures = HashMap<Option<KeywordMatcher>, ArgumentSchema>;

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug, FromPyObject)]
pub enum CommandSchemaDetails {
    StandardCommand {
        schema: ArgumentSchema,
        signatures: Signatures,
        two_words_keywords: Vec<KeywordMatcher>,
    },
    SpecializedCommand {
        #[pyo3(attribute("impl"))]
        specialization: String,
    },
}

#[derive(Clone, Debug, FromPyObject)]
pub struct CommandSchema {
    pub block_end: Option<String>,
    pub canonical_name: Option<String>,
    pub inhibit_favour_expansion: bool,
    pub details: CommandSchemaDetails,
}

pub type CommandSchemaMapping = HashMap<String, CommandSchema>;

pub struct CommandSchemas {
    pub definition_schemas: CommandSchemaMapping,
    pub extension_schemas: CommandSchemaMapping,
    pub blocks: Vec<(String, BlockCommand)>,
}

pub struct BlockCommand {
    pub pattern: String,
}

fn prepare_blocks<'a, Schemas>(schemas: Schemas) -> Vec<(String, BlockCommand)>
where
    Schemas: Iterator<Item = &'a CommandSchema>,
{
    schemas
        .filter_map(|schema| match schema {
            CommandSchema {
                canonical_name: Some(canonical_name),
                block_end: Some(block_end),
                ..
            } => Some((
                canonical_name.trim().to_lowercase(),
                BlockCommand {
                    pattern: block_end.clone(),
                },
            )),
            _ => None,
        })
        .collect()
}

impl CommandSchemas {
    pub fn new(
        definition_schemas: CommandSchemaMapping,
        extension_schemas: CommandSchemaMapping,
    ) -> Self {
        let blocks = prepare_blocks(
            definition_schemas
                .values()
                .chain(extension_schemas.values())
                .chain(builtin_schemas().values()),
        );
        Self {
            definition_schemas,
            extension_schemas,
            blocks,
        }
    }

    pub fn default() -> Self {
        Self {
            definition_schemas: HashMap::new(),
            extension_schemas: HashMap::new(),
            blocks: prepare_blocks(builtin_schemas().values()),
        }
    }

    pub fn get_impl(&self, key: &str) -> Option<&CommandSchema> {
        self.definition_schemas.get(key).or_else(|| {
            self.extension_schemas
                .get(key)
                .or_else(|| builtin_schemas().get(key))
        })
    }

    pub fn get(&self, key: &str) -> Option<&CommandSchema> {
        self.get_impl(key)
            .or_else(|| self.get_impl(&key.to_lowercase()))
    }

    pub fn contains_key(&self, key: &str) -> bool {
        self.get(key).is_some()
    }
}
