use enum_iterator::{all, Sequence};
#[cfg(debug_assertions)]
use gloo::console;
use serde::Serialize;
use std::fmt::{Display, Formatter};
use yew::prelude::*;

use kanidm_proto::internal::Filter::{Eq, Or};
use kanidm_proto::internal::{SearchRequest, SearchResponse};
use kanidm_proto::v1::Entry;
use kanidmd_web_ui_shared::constants::CSS_PAGE_HEADER;
use kanidmd_web_ui_shared::{do_request, error::FetchError, RequestMethod};
use wasm_bindgen::prelude::*;
use web_sys::HtmlInputElement;
use yew_router::Routable;

use crate::router::AdminRoute;
use kanidmd_web_ui_shared::ui::{error_page, loading_spinner};
use kanidmd_web_ui_shared::utils::{init_graphviz, open_blank};

pub enum Msg {
    NewFilters { filters: Vec<ObjectType> },
    NewObjects { entries: Vec<Entry> },

    Error { emsg: String, kopid: Option<String> },
}

impl From<FetchError> for Msg {
    fn from(fe: FetchError) -> Self {
        Msg::Error {
            emsg: fe.as_string(),
            kopid: None,
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Sequence)]
pub enum ObjectType {
    Group,
    BuiltinGroup,
    ServiceAccount,
    Person,
}

impl Display for ObjectType {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let str = match self {
            ObjectType::Group => "Group",
            ObjectType::BuiltinGroup => "Built In Group",
            ObjectType::ServiceAccount => "Service Account",
            ObjectType::Person => "Person",
        };

        write!(f, "{str}")
    }
}

impl TryFrom<String> for ObjectType {
    type Error = ();

    fn try_from(value: String) -> Result<Self, Self::Error> {
        all::<ObjectType>()
            .find(|x| format!("{x}") == value)
            .ok_or(())
    }
}

pub enum State {
    Waiting,
    Ready { entries: Vec<Entry> },
    Error { emsg: String, kopid: Option<String> },
}

pub struct AdminObjectGraph {
    state: State,
    filters: Vec<ObjectType>,
}

impl Component for AdminObjectGraph {
    type Message = Msg;
    type Properties = ();

    fn create(ctx: &Context<Self>) -> Self {
        #[cfg(debug_assertions)]
        console::debug!("views::objectgraph::create");

        ctx.link()
            .send_future(async { Self::fetch_objects().await.unwrap_or_else(|v| v.into()) });

        let state = State::Waiting;

        AdminObjectGraph {
            state,
            filters: vec![],
        }
    }

    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
        #[cfg(debug_assertions)]
        console::debug!("views::objectgraph::update");
        match msg {
            Msg::NewObjects { entries } => {
                #[cfg(debug_assertions)]
                console::debug!("Received new objects");
                self.state = State::Ready { entries }
            }
            Msg::NewFilters { filters } => {
                #[cfg(debug_assertions)]
                console::debug!("Received new filters");
                self.filters = filters;
            }
            Msg::Error { emsg, kopid } => self.state = State::Error { emsg, kopid },
        }

        true
    }

    fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
        #[cfg(debug_assertions)]
        console::debug!("views::objectgraph::changed");
        false
    }

    fn view(&self, ctx: &Context<Self>) -> Html {
        match &self.state {
            State::Waiting => self.view_waiting(),
            State::Ready { entries } => self.view_ready(ctx, &self.filters, entries),
            State::Error { emsg, kopid } => self.view_error(ctx, emsg, kopid.as_deref()),
        }
    }

    fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
        #[cfg(debug_assertions)]
        console::debug!("views::objectgraph::rendered");
    }
}

impl AdminObjectGraph {
    fn view_waiting(&self) -> Html {
        loading_spinner()
    }

    fn view_ready(&self, ctx: &Context<Self>, filters: &[ObjectType], entries: &[Entry]) -> Html {
        let typed_entries = entries
            .iter()
            .filter_map(|entry| {
                let classes = entry.attrs.get("class")?;
                let uuid = entry.attrs.get("uuid")?.first()?;

                // Logic to decide the type of each entry
                let obj_type = if classes.contains(&"group".to_string()) {
                    if uuid.starts_with("00000000-0000-0000-0000-") {
                        ObjectType::BuiltinGroup
                    } else {
                        ObjectType::Group
                    }
                } else if classes.contains(&"account".to_string()) {
                    if classes.contains(&"person".to_string()) {
                        ObjectType::Person
                    } else if classes.contains(&"service-account".to_string()) {
                        ObjectType::ServiceAccount
                    } else {
                        return None;
                    }
                } else {
                    return None;
                };

                // Filter out the things we want to keep, if the filter is empty we assume we want all.
                if !filters.contains(&obj_type) && !filters.is_empty() {
                    return None;
                }

                let spn = entry.attrs.get("spn")?.first()?;
                Some((spn.clone(), uuid.clone(), obj_type))
            })
            .collect::<Vec<(String, String, ObjectType)>>();

        // Vec<obj, uuid, obj's members>
        let members_of = entries
            .iter()
            .filter_map(|entry| {
                let spn = entry.attrs.get("spn")?.first()?.clone();
                let uuid = entry.attrs.get("uuid")?.first()?.clone();
                let keep = typed_entries
                    .iter()
                    .any(|(_, filtered_uuid, _)| &uuid == filtered_uuid);
                if keep {
                    Some((spn, uuid, entry.attrs.get("member")?.clone()))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();

        // Constructing graph source
        let mut sb = String::new();
        sb.push_str("digraph {\n  rankdir=\"RL\"\n");
        for (spn, _, members) in members_of {
            members
                .iter()
                .filter(|member| typed_entries.iter().any(|(spn, _, _)| spn == *member))
                .for_each(|member| {
                    sb.push_str(format!(r#"  "{spn}" -> "{member}"{}"#, "\n").as_str());
                });
        }

        for (spn, uuid, obj_type) in typed_entries {
            let (color, shape, route) = match obj_type {
                ObjectType::Group => ("#b86367", "box", AdminRoute::ViewGroup { id_or_name: uuid }),
                ObjectType::BuiltinGroup => (
                    "#8bc1d6",
                    "component",
                    AdminRoute::ViewGroup { id_or_name: uuid },
                ),
                ObjectType::ServiceAccount => (
                    "#77c98d",
                    "parallelogram",
                    AdminRoute::ViewServiceAccount { id_or_name: uuid },
                ),
                ObjectType::Person => (
                    "#af8bd6",
                    "ellipse",
                    AdminRoute::ViewPerson { id_or_name: uuid },
                ),
            };
            let url = route.to_path();
            sb.push_str(
                format!(
                    r#"  "{spn}" [color = "{color}", shape = {shape}, URL = "{url}"]{}"#,
                    "\n"
                )
                .as_str(),
            );
        }
        sb.push('}');
        init_graphviz(sb.as_str());

        let node_refs = all::<ObjectType>()
            .map(|object_type: ObjectType| (object_type, NodeRef::default()))
            .collect::<Vec<_>>();

        let on_checkbox_click = {
            let scope = ctx.link().clone();
            let node_refs = node_refs.clone();
            move |_: Event| {
                let mut filters = vec![];

                for (obj_type, node_ref) in &node_refs {
                    if let Some(input_el) = node_ref.cast::<HtmlInputElement>() {
                        let checked = input_el.checked();

                        if checked {
                            filters.push(*obj_type);
                        }
                    }
                }
                scope.send_message(Msg::NewFilters { filters });
            }
        };

        let view_graph_source = {
            move |_: MouseEvent| {
                open_blank(sb.as_str());
            }
        };

        html! {
            <>
            <div class={CSS_PAGE_HEADER}>
            <h2>{ "ObjectGraph view" }</h2>
            </div>
            if entries.is_empty() {
                <div>
                  <h5>{ "No graph objects for the applied filters." }</h5>
                </div>
            } else {
                <div class="column">
                    <div class="hstack gap-3">
                    {
                        node_refs.iter().map(|(ot, node_ref)| {
                            let ot_str = format!("{}", ot);
                            let selected = filters.contains(ot);

                            html! {
                                <>
                                <div class="form-check">
                                  <input class="form-check-input obj-graph-filter-cb" type="checkbox" ref={ node_ref } id={ot_str.clone()} onchange={on_checkbox_click.clone()} checked={selected}/>
                                  <label class="form-check-label" for={ot_str.clone()}>{ot_str.clone()}</label>
                                </div>
                                if *ot != ObjectType::last().unwrap() {
                                    <div class="vr"></div>
                                }
                                </>
                            }
                        }).collect::<Html>()
                    }
                    </div>
                    <button class="btn btn-primary mt-2" onclick={view_graph_source}>{ "View graph source" }</button>
                </div>
                <div id="graph-container" class="mt-3"></div>
            }
            </>
        }
    }

    fn view_error(&self, _ctx: &Context<Self>, msg: &str, kopid: Option<&str>) -> Html {
        error_page(msg, kopid)
    }

    async fn fetch_objects() -> Result<Msg, FetchError> {
        let req_body = SearchRequest {
            filter: Or(vec![
                Eq("class".to_string(), "person".to_string()),
                Eq("class".to_string(), "service_account".to_string()),
                Eq("class".to_string(), "group".to_string()),
            ]),
        };

        let req_jsvalue = req_body
            .serialize(&serde_wasm_bindgen::Serializer::json_compatible())
            .expect("Failed to serialise request");
        let req_jsvalue = js_sys::JSON::stringify(&req_jsvalue).expect_throw("failed to stringify");

        let (kopid, status, value, _) =
            do_request("/v1/raw/search", RequestMethod::POST, Some(req_jsvalue)).await?;

        if status == 200 {
            let search_resp: SearchResponse = serde_wasm_bindgen::from_value(value)
                .expect_throw("Invalid response type - objectgraph::SearchRequest");

            Ok(Msg::NewObjects {
                entries: search_resp.entries,
            })
        } else {
            let emsg = value.as_string().unwrap_or_default();
            Ok(Msg::Error { emsg, kopid })
        }
    }
}