use std::time::SystemTime;

use chrono::{DateTime, Local, Locale};
use minijinja::{context, Environment};

use liwe::model::node::NodeIter;
use liwe::model::node::{Node, Reference, ReferenceType};
use liwe::model::tree::Tree;
use liwe::model::Key;
use liwe::operations::Changes;

use super::{Action, ActionContext, ActionProvider};

pub struct AttachAction {
    pub title: String,
    pub identifier: String,

    pub key_template: String,
    pub document_template: String,
    pub content_date_format: String,
    pub content_time_format: String,
    pub key_date_format: String,
    pub key_time_format: String,
    pub key_locale: Locale,
    pub content_locale: Locale,
}

impl AttachAction {
    fn format_target_key(&self, now: SystemTime) -> Key {
        let now: DateTime<Local> = now.into();
        let today_formatted = now
            .format_localized(&self.key_date_format, self.key_locale)
            .to_string();
        let now_formatted = now
            .format_localized(&self.key_time_format, self.key_locale)
            .to_string();

        Key::name(
            &Environment::new()
                .template_from_str(&self.key_template)
                .expect("correct template")
                .render(context! {
                today => today_formatted,
                now => now_formatted,
                })
                .expect("template to work"),
        )
    }

    fn format_target_document(&self, now: SystemTime, content: String) -> String {
        let now: DateTime<Local> = now.into();
        let today_formatted = now
            .format_localized(&self.content_date_format, self.content_locale)
            .to_string();
        let now_formatted = now
            .format_localized(&self.content_time_format, self.content_locale)
            .to_string();
        Environment::new()
            .template_from_str(&self.document_template)
            .expect("correct template")
            .render(context! {
            today => today_formatted,
            now => now_formatted,
            content => content
            })
            .expect("template to work")
    }
}

impl ActionProvider for AttachAction {
    fn identifier(&self) -> String {
        format!("custom.{}", self.identifier)
    }

    fn action(
        &self,
        key: Key,
        selection: super::TextRange,
        context: impl ActionContext,
    ) -> Option<Action> {
        let target_id = context.get_node_id_at(&key, selection.start.line as usize)?;
        let tree = context.collect(&key);

        let (reference_key, _reference_text) = if tree.get(target_id).is_reference() {
            let ref_key = tree.find_reference_key(target_id);
            let ref_text = tree
                .find_id(target_id)
                .and_then(|t| t.node.reference_text())
                .unwrap_or_default();
            (ref_key, ref_text)
        } else {
            let ref_key = context.get_link_key_at(
                &key,
                selection.start.line as usize,
                selection.start.character as usize,
            )?;
            let ref_text = context
                .get_link_text_at(
                    &key,
                    selection.start.line as usize,
                    selection.start.character as usize,
                )
                .unwrap_or_default();
            (ref_key, ref_text)
        };

        let now = context.now();
        let attach_to_key = self.format_target_key(now);

        if context.key_exists(&attach_to_key)
            && context
                .collect(&attach_to_key)
                .get_all_inclusion_edge_keys()
                .contains(&reference_key)
        {
            return None;
        }

        context.graph().maybe_key(&reference_key)?;

        Some(Action {
            title: self.title.clone(),
            identifier: self.identifier(),
            key: key.clone(),
            range: selection.clone(),
        })
    }

    fn changes(
        &self,
        key: Key,
        selection: super::TextRange,
        context: impl ActionContext,
    ) -> Option<Changes> {
        let target_id = context.get_node_id_at(&key, selection.start.line as usize)?;
        let tree = context.collect(&key);

        let (reference_key, reference_text) = if tree.get(target_id).is_reference() {
            let ref_key = tree.find_reference_key(target_id);
            let ref_text = tree
                .find_id(target_id)
                .and_then(|t| t.node.reference_text())
                .unwrap_or_default();
            (ref_key, ref_text)
        } else {
            let ref_key = context.get_link_key_at(
                &key,
                selection.start.line as usize,
                selection.start.character as usize,
            )?;
            let ref_text = context
                .get_link_text_at(
                    &key,
                    selection.start.line as usize,
                    selection.start.character as usize,
                )
                .unwrap_or_default();
            (ref_key, ref_text)
        };

        let now = context.now();
        let attach_to_key = self.format_target_key(now);
        let reference = Tree {
            id: None,
            node: Node::Reference(Reference {
                key: reference_key,
                text: reference_text,
                reference_type: ReferenceType::Regular,
                url: String::new(),
                display_url: None,
            }),
            children: vec![],
        };

        if context.key_exists(&attach_to_key) {
            let tree = context.collect(&attach_to_key);

            let updated = tree.attach(reference);

            Some(
                Changes::new().update(
                    attach_to_key.clone(),
                    updated
                        .iter()
                        .to_text(&attach_to_key.parent(), &context.format_options()),
                ),
            )
        } else {
            Some(
                Changes::new().create(
                    attach_to_key.clone(),
                    self.format_target_document(
                        now,
                        reference
                            .iter()
                            .to_text(&attach_to_key.parent(), &context.format_options()),
                    ),
                ),
            )
        }
    }
}
