use std::collections::HashMap;

use liwe::graph::Graph;
use liwe::locale::get_locale;
use liwe::model::config::{
    ActionDefinition, Command, Configuration, MarkdownOptions, DEFAULT_KEY_DATE_FORMAT,
};
use liwe::model::tree::Tree;
use liwe::model::{Key, Markdown, NodeId};

use lsp_types::{CodeAction, CodeActionKind, CodeActionOrCommand};
use once_cell::sync::Lazy;
use std::sync::Mutex;

use super::{BasePath, ChangeExt};

mod attach;
mod delete;
mod extract;
mod extract_all;
mod inline;
mod link;
mod list;
mod section;
mod sort;
pub mod templates;
mod transform;

pub use attach::AttachAction;
pub use delete::DeleteAction;
pub use extract::SectionExtract;
pub use extract_all::ExtractAll;
pub use inline::InlineAction;
pub use link::LinkAction;
pub use list::{ListChangeType, ListToSections};
pub use section::SectionToList;
pub use sort::SortAction;
pub use transform::TransformBlockAction;

pub trait ActionContext {
    fn key_of(&self, node_id: NodeId) -> Key;
    fn key_exists(&self, key: &Key) -> bool;
    fn collect(&self, key: &Key) -> Tree;
    fn squash(&self, key: &Key, depth: u8) -> Tree;
    fn random_key(&self, parent: &str) -> Key;
    fn random_keys(&self, parent: &str, number: usize) -> Vec<Key>;
    fn unique_ids(&self, parent: &str, number: usize) -> Vec<String>;
    fn markdown_options(&self) -> &MarkdownOptions;
    fn get_command(&self, name: &str) -> Option<&Command>;
    fn graph(&self) -> &Graph;
    fn patch(&self) -> Graph;
    fn get_inclusion_edges_to(&self, key: &Key) -> Vec<NodeId>;
    fn get_reference_edges_to(&self, key: &Key) -> Vec<NodeId>;
    fn get_ref_text(&self, key: &Key) -> Option<String>;
    fn get_node_id_at(&self, key: &Key, line: usize) -> Option<NodeId>;
    fn get_document_markdown(&self, key: &Key) -> Option<String>;
    fn get_link_key_at(&self, key: &Key, line: usize, character: usize) -> Option<Key>;
    fn get_link_text_at(&self, key: &Key, line: usize, character: usize) -> Option<String>;
    fn now(&self) -> std::time::SystemTime;
}

#[derive(Clone)]
pub struct Action {
    pub title: String,
    pub identifier: String,
    pub key: Key,
    pub range: TextRange,
}

#[derive(Clone)]
pub struct Position {
    pub line: u32,
    pub character: u32,
}

#[derive(Clone)]
pub struct TextRange {
    pub start: Position,
    pub end: Position,
}

pub enum Change {
    Remove(Remove),
    Create(Create),
    Update(Update),
}

pub type Changes = Vec<Change>;

pub struct Create {
    pub key: Key,
}

pub struct Update {
    pub key: Key,
    pub markdown: Markdown,
}

pub struct Remove {
    pub key: Key,
}

impl Action {
    pub fn to_code_action(&self) -> CodeActionOrCommand {
        CodeActionOrCommand::CodeAction(CodeAction {
            title: self.title.to_string(),
            kind: Some(identifier_to_action_kind(self.identifier.to_string())),
            data: Some(serde_json::json!({
                "key": self.key.to_string(),
                "range": {
                    "start": {
                        "line": self.range.start.line,
                        "character": self.range.start.character,
                    },
                    "end": {
                        "line": self.range.end.line,
                        "character": self.range.end.character,
                    }
                }
            })),
            ..Default::default()
        })
    }

    pub fn resolve_code_action(
        &self,
        base_path: &BasePath,
        changes: Changes,
    ) -> CodeActionOrCommand {
        use itertools::Itertools;
        use lsp_types::{DocumentChanges, WorkspaceEdit};

        CodeActionOrCommand::CodeAction(CodeAction {
            title: self.title.to_string(),
            kind: Some(identifier_to_action_kind(self.identifier.to_string())),
            edit: Some(WorkspaceEdit {
                document_changes: Some(DocumentChanges::Operations(
                    changes
                        .iter()
                        .map(|change| change.to_document_change(base_path))
                        .collect_vec(),
                )),
                ..Default::default()
            }),
            ..Default::default()
        })
    }
}

pub trait ActionProvider {
    fn identifier(&self) -> String;
    fn action(&self, key: Key, selection: TextRange, context: impl ActionContext)
        -> Option<Action>;
    fn changes(
        &self,
        key: Key,
        selection: TextRange,
        context: impl ActionContext,
    ) -> Option<liwe::operations::Changes>;

    fn action_kind(&self) -> CodeActionKind {
        identifier_to_action_kind(self.identifier())
    }
}

pub enum ActionEnum {
    ListChangeType(ListChangeType),
    ListToSections(ListToSections),
    SectionToList(SectionToList),
    SectionExtract(SectionExtract),
    ExtractAll(ExtractAll),
    TransformBlockAction(TransformBlockAction),
    AttachAction(AttachAction),
    SortAction(SortAction),
    InlineAction(InlineAction),
    DeleteAction(DeleteAction),
    LinkAction(LinkAction),
}

impl ActionProvider for ActionEnum {
    fn identifier(&self) -> String {
        match self {
            ActionEnum::ListChangeType(inner) => inner.identifier(),
            ActionEnum::ListToSections(inner) => inner.identifier(),
            ActionEnum::SectionToList(inner) => inner.identifier(),
            ActionEnum::SectionExtract(inner) => inner.identifier(),
            ActionEnum::ExtractAll(inner) => inner.identifier(),
            ActionEnum::TransformBlockAction(inner) => inner.identifier(),
            ActionEnum::AttachAction(inner) => inner.identifier(),
            ActionEnum::SortAction(inner) => inner.identifier(),
            ActionEnum::InlineAction(inner) => inner.identifier(),
            ActionEnum::DeleteAction(inner) => inner.identifier(),
            ActionEnum::LinkAction(inner) => inner.identifier(),
        }
    }

    fn action(
        &self,
        key: Key,
        selection: TextRange,
        context: impl ActionContext,
    ) -> Option<Action> {
        match self {
            ActionEnum::ListChangeType(inner) => inner.action(key, selection, context),
            ActionEnum::ListToSections(inner) => inner.action(key, selection, context),
            ActionEnum::SectionToList(inner) => inner.action(key, selection, context),
            ActionEnum::SectionExtract(inner) => inner.action(key, selection, context),
            ActionEnum::ExtractAll(inner) => inner.action(key, selection, context),
            ActionEnum::TransformBlockAction(inner) => inner.action(key, selection, context),
            ActionEnum::AttachAction(inner) => inner.action(key, selection, context),
            ActionEnum::SortAction(inner) => inner.action(key, selection, context),
            ActionEnum::InlineAction(inner) => inner.action(key, selection, context),
            ActionEnum::DeleteAction(inner) => inner.action(key, selection, context),
            ActionEnum::LinkAction(inner) => inner.action(key, selection, context),
        }
    }

    fn changes(
        &self,
        key: Key,
        selection: TextRange,
        context: impl ActionContext,
    ) -> Option<liwe::operations::Changes> {
        match self {
            ActionEnum::ListChangeType(inner) => inner.changes(key, selection, context),
            ActionEnum::ListToSections(inner) => inner.changes(key, selection, context),
            ActionEnum::SectionToList(inner) => inner.changes(key, selection, context),
            ActionEnum::SectionExtract(inner) => inner.changes(key, selection, context),
            ActionEnum::ExtractAll(inner) => inner.changes(key, selection, context),
            ActionEnum::TransformBlockAction(inner) => inner.changes(key, selection, context),
            ActionEnum::AttachAction(inner) => inner.changes(key, selection, context),
            ActionEnum::SortAction(inner) => inner.changes(key, selection, context),
            ActionEnum::InlineAction(inner) => inner.changes(key, selection, context),
            ActionEnum::DeleteAction(inner) => inner.changes(key, selection, context),
            ActionEnum::LinkAction(inner) => inner.changes(key, selection, context),
        }
    }
}

static CODE_ACTION_MAP: Lazy<Mutex<HashMap<String, CodeActionKind>>> =
    Lazy::new(|| Mutex::new(HashMap::new()));

pub fn identifier_to_action_kind(identifier: String) -> CodeActionKind {
    let mut map = CODE_ACTION_MAP
        .lock()
        .expect("CODE_ACTION_MAP mutex poisoned");
    map.entry(identifier.clone())
        .or_insert_with(|| CodeActionKind::new(identifier.clone().leak()))
        .clone()
}

pub fn all_actions() -> Vec<ActionEnum> {
    vec![
        ActionEnum::ListChangeType(ListChangeType {}),
        ActionEnum::ListToSections(ListToSections {}),
        ActionEnum::SectionToList(SectionToList {}),
    ]
}

pub fn all_action_types(configuration: &Configuration) -> Vec<ActionEnum> {
    let mut actions = vec![
        ActionEnum::ListChangeType(ListChangeType {}),
        ActionEnum::ListToSections(ListToSections {}),
        ActionEnum::SectionToList(SectionToList {}),
        ActionEnum::DeleteAction(DeleteAction {}),
    ];

    let key_locale = get_locale(configuration.library.locale.as_deref());
    let markdown_locale = get_locale(configuration.markdown.locale.as_deref());

    actions.extend(configuration.actions.iter().map(|(identifier, action)| {
        match action {
            ActionDefinition::Transform(transform) => {
                ActionEnum::TransformBlockAction(TransformBlockAction {
                    title: transform.title.clone(),
                    identifier: identifier.clone(),
                    command: transform.command.clone(),
                    input_template: transform.input_template.clone(),
                })
            }
            ActionDefinition::Attach(attach) => {
                let md_date_fmt = configuration
                    .clone()
                    .markdown
                    .date_format
                    .unwrap_or("%b %d, %Y".into());
                let key_date_fmt = configuration
                    .clone()
                    .library
                    .date_format
                    .unwrap_or(DEFAULT_KEY_DATE_FORMAT.into());
                ActionEnum::AttachAction(AttachAction {
                    title: attach.title.clone(),
                    identifier: identifier.clone(),
                    document_template: attach.document_template.clone(),
                    key_template: attach.key_template.clone(),
                    markdown_date_format: md_date_fmt.clone(),
                    markdown_time_format: configuration
                        .clone()
                        .markdown
                        .time_format
                        .unwrap_or_else(|| md_date_fmt.clone()),
                    key_date_format: key_date_fmt.clone(),
                    key_time_format: configuration
                        .clone()
                        .library
                        .time_format
                        .unwrap_or_else(|| key_date_fmt.clone()),
                    key_locale,
                    markdown_locale,
                })
            }
            ActionDefinition::Sort(sort) => ActionEnum::SortAction(SortAction {
                title: sort.title.clone(),
                identifier: identifier.clone(),
                reverse: sort.reverse.unwrap_or(false),
            }),
            ActionDefinition::Inline(inline) => ActionEnum::InlineAction(InlineAction {
                title: inline.title.clone(),
                identifier: identifier.clone(),
                inline_type: inline.inline_type.clone(),
                keep_target: inline.keep_target.unwrap_or(false),
            }),
            ActionDefinition::Extract(extract) => ActionEnum::SectionExtract(SectionExtract {
                title: extract.title.clone(),
                identifier: identifier.clone(),
                link_type: extract.link_type.clone(),
                key_template: extract.key_template.clone(),
                key_date_format: configuration
                    .clone()
                    .library
                    .date_format
                    .unwrap_or(DEFAULT_KEY_DATE_FORMAT.into()),
                locale: key_locale,
            }),
            ActionDefinition::ExtractAll(extract_all) => ActionEnum::ExtractAll(ExtractAll {
                title: extract_all.title.clone(),
                identifier: identifier.clone(),
                link_type: extract_all.link_type.clone(),
                key_template: extract_all.key_template.clone(),
                key_date_format: configuration
                    .clone()
                    .library
                    .date_format
                    .unwrap_or(DEFAULT_KEY_DATE_FORMAT.into()),
                locale: key_locale,
            }),
            ActionDefinition::Link(link) => ActionEnum::LinkAction(LinkAction {
                title: link.title.clone(),
                identifier: identifier.clone(),
                link_type: link.link_type.clone(),
                key_template: link.key_template.clone(),
                key_date_format: configuration
                    .clone()
                    .library
                    .date_format
                    .unwrap_or(DEFAULT_KEY_DATE_FORMAT.into()),
                locale: key_locale,
            }),
        }
    }));

    actions
}

pub use liwe::operations::string_to_slug;

pub fn into_lsp_changes(liwe_changes: liwe::operations::Changes) -> Changes {
    let mut changes = Vec::new();

    for (key, markdown) in liwe_changes.creates {
        changes.push(Change::Create(Create { key: key.clone() }));
        changes.push(Change::Update(Update { key, markdown }));
    }
    for key in liwe_changes.removes {
        changes.push(Change::Remove(Remove { key }));
    }
    for (key, markdown) in liwe_changes.updates {
        changes.push(Change::Update(Update { key, markdown }));
    }

    changes
}
