use crate::codec::ron::RonEncoded;
use crate::components::OnHoverRed;
use crate::components::ProblemInfo;
use crate::components::attempt::Attempt;
use crate::components::button::Button;
use crate::components::icons::Icon;
use crate::gradient::Gradient;
use crate::models;
use crate::models::HoldRole;
use crate::server_functions;
use leptos::Params;
use leptos::prelude::*;
use leptos_router::params::Params;
use std::collections::BTreeMap;
use std::collections::BTreeSet;
use std::ops::Deref;

#[derive(Params, PartialEq, Clone)]
struct RouteParams {
    wall_uid: Option<models::WallUid>,
}

#[component]
#[tracing::instrument(skip_all)]
pub fn Page() -> impl IntoView {
    crate::tracing::on_enter!();

    let route_params = leptos_router::hooks::use_params::<RouteParams>();

    let wall_uid = Signal::derive(move || {
        route_params
            .get()
            .expect("gets wall_uid from URL")
            .wall_uid
            .expect("wall_uid param is never None")
    });

    let wall = crate::resources::wall_by_uid(wall_uid);
    let user_interactions = crate::resources::user_interactions_for_wall(wall_uid);

    leptos::view! {
        <div class="min-h-screen min-w-screen bg-neutral-950">
            <Suspense fallback=|| {
                "loading"
            }>
                {move || Suspend::new(async move {
                    tracing::debug!("executing main suspend");
                    let wall = wall.await?;
                    let user_interactions = user_interactions.await?;
                    let user_interactions = RwSignal::new(user_interactions);
                    Ok::<_, ServerFnError>(view! { <Controller wall user_interactions /> })
                })}
            </Suspense>
        </div>
    }
}

#[derive(Debug, Clone, Copy)]
struct Context {
    wall: Signal<models::Wall>,
    user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
    problem: Signal<Option<models::Problem>>,
    filtered_problems: Signal<BTreeSet<models::Problem>>,
    user_interaction: Signal<Option<models::UserInteraction>>,
    todays_attempt: Signal<Option<models::Attempt>>,
    latest_attempt: Signal<Option<models::DatedAttempt>>,
    filter_holds: Signal<BTreeSet<models::HoldPosition>>,
    cb_click_hold: Callback<models::HoldPosition>,
    cb_remove_hold_from_filter: Callback<models::HoldPosition>,
    cb_next_problem: Callback<()>,
    cb_set_problem: Callback<models::Problem>,
    cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
}

#[component]
#[tracing::instrument(skip_all)]
fn Controller(
    #[prop(into)] wall: Signal<models::Wall>,
    #[prop(into)] user_interactions: RwSignal<BTreeMap<models::Problem, models::UserInteraction>>,
) -> impl IntoView {
    crate::tracing::on_enter!();

    // Extract data from URL
    let (problem, set_problem) = leptos_router::hooks::query_signal::<models::Problem>("problem");

    // Filter
    let (filter_holds, set_filter_holds) = signal(BTreeSet::new());
    let cb_remove_hold_from_filter: Callback<models::HoldPosition> = Callback::new(move |hold_pos: models::HoldPosition| {
        set_filter_holds.update(move |set| {
            set.remove(&hold_pos);
        });
    });

    // Derive signals
    let user_interaction = signals::user_interaction(user_interactions.into(), problem.into());
    let filtered_problems = signals::filtered_problems(wall, filter_holds.into());
    let todays_attempt = signals::todays_attempt(user_interaction);
    let latest_attempt = signals::latest_attempt(user_interaction);

    // Submit attempt action
    let upsert_todays_attempt = ServerAction::<RonEncoded<server_functions::UpsertTodaysAttempt>>::new();
    let cb_upsert_todays_attempt = Callback::new(move |attempt| {
        upsert_todays_attempt.dispatch(RonEncoded(attempt));
    });

    // Callback: Set specific problem
    let cb_set_problem: Callback<models::Problem> = Callback::new(move |problem| {
        set_problem.set(Some(problem));
    });

    // Callback: Set next problem to a random problem
    let cb_set_random_problem: Callback<()> = Callback::new(move |_| {
        // TODO: remove current problem from population
        let population = filtered_problems.read();
        let population = population.deref();

        use rand::seq::IteratorRandom;
        let mut rng = rand::rng();
        let problem = population.iter().choose(&mut rng);

        set_problem.set(problem.cloned());
    });

    // Callback: On click hold, Add/Remove hold position to problem filter
    let cb_click_hold: Callback<models::HoldPosition> = Callback::new(move |hold_position| {
        set_filter_holds.update(|set| {
            if !set.remove(&hold_position) {
                set.insert(hold_position);
            }
        });
    });

    // Set a problem when wall is set (loaded)
    Effect::new(move |_prev_value| {
        if problem.read().is_none() {
            tracing::debug!("Setting initial problem");
            cb_set_random_problem.run(());
        }
    });

    // Update user interactions after submitting an attempt
    Effect::new(move || {
        if let Some(Ok(v)) = upsert_todays_attempt.value().get() {
            let v = v.into_inner();
            user_interactions.update(|map| {
                map.insert(v.problem.clone(), v);
            });
        }
    });

    provide_context(Context {
        wall,
        problem: problem.into(),
        cb_click_hold,
        user_interaction,
        latest_attempt,
        cb_upsert_todays_attempt,
        cb_remove_hold_from_filter,
        cb_next_problem: cb_set_random_problem,
        cb_set_problem,
        todays_attempt,
        filter_holds: filter_holds.into(),
        filtered_problems: filtered_problems.into(),
        user_interactions: user_interactions.into(),
    });

    view! { <View /> }
}

#[component]
#[tracing::instrument(skip_all)]
fn View() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    view! {
        <div class="flex">
            <div class="flex-initial">
                <Wall />
            </div>

            <div class="flex flex-col px-2 pt-3" style="width:38rem">
                <Section title="Problems">
                    <Filter />

                    <Separator />

                    <div class="flex flex-row justify-around">
                        <NextProblemButton />
                    </div>
                </Section>

                <Separator />

                <Section title="Current problem">
                    {move || ctx.problem.get().map(|problem| view! { <ProblemInfo problem /> })}
                    <Separator /> <Transformations /> <Separator /><AttemptRadioGroup />
                    <Separator /> <History />
                </Section>
            </div>

            <div class="flex flex-row flex-auto justify-end items-start px-2 pt-3">
                <HoldsButton />
            </div>
        </div>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn Transformations() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    let left = Signal::derive(move || {
        let mut problem = ctx.problem.get()?;
        let new_pattern = problem.pattern.shift_left(1)?;
        problem.pattern = new_pattern;
        Some(problem)
    });

    let right = Signal::derive(move || {
        let mut problem = ctx.problem.get()?;
        let wall_dimensions = ctx.wall.read().wall_dimensions;
        let new_pattern = problem.pattern.shift_right(wall_dimensions, 1)?;
        problem.pattern = new_pattern;
        Some(problem)
    });

    let on_click_left = Callback::new(move |()| {
        tracing::debug!("left");
        if let Some(problem) = left.get() {
            ctx.cb_set_problem.run(problem);
        }
    });
    let on_click_mirror = Callback::new(move |()| {
        tracing::debug!("mirror");
        if let Some(mut problem) = ctx.problem.get() {
            problem.pattern = problem.pattern.mirror();
            ctx.cb_set_problem.run(problem);
        }
    });
    let on_click_right = Callback::new(move |()| {
        tracing::debug!("right");
        if let Some(problem) = right.get() {
            ctx.cb_set_problem.run(problem);
        }
    });

    let left_disabled = Signal::derive(move || left.read().is_none());
    let right_disabled = Signal::derive(move || right.read().is_none());

    view! {
        <div class="flex flex-row gap-2 justify-center">
            <Button icon=Icon::ChevronLeft disabled=left_disabled on_click=on_click_left />
            <Button icon=Icon::CodeBracketSquare on_click=on_click_mirror />
            <Button icon=Icon::ChevronRight disabled=right_disabled on_click=on_click_right />
        </div>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn HoldsButton() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    let link = move || format!("/wall/{}/edit", ctx.wall.read().uid);

    view! {
        <a href=link>
            <Button text="Holds" icon=Icon::WrenchSolid />
        </a>
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn NextProblemButton() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    let on_click = Callback::new(move |_| ctx.cb_next_problem.run(()));
    view! { <Button icon=Icon::ArrowPath text="Randomize" on_click color=Gradient::PurpleBlue /> }
}

#[component]
#[tracing::instrument(skip_all)]
fn Filter() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    move || {
        let mut cells = vec![];
        for hold_pos in ctx.filter_holds.get() {
            let w = &*ctx.wall.read();
            if let Some(hold) = w.holds.get(&hold_pos).cloned() {
                let onclick = move |_| {
                    ctx.cb_remove_hold_from_filter.run(hold_pos);
                };

                let v = view! {
                    <button on:click=onclick class="cursor-pointer">
                        <OnHoverRed>
                            <Hold hold />
                        </OnHoverRed>
                    </button>
                };
                cells.push(v);
            }
        }

        let problems_counter = {
            let name = view! { <p class="mr-4 font-light text-right text-orange-300">{"Problems:"}</p> };
            let value = view! { <p class="text-white">{ctx.filtered_problems.read().len()}</p> };
            view! {
                <div class="grid grid-rows-none gap-x-0.5 gap-y-1 grid-cols-[auto_1fr]">
                    {name} {value}
                </div>
            }
        };

        let sep = (!cells.is_empty()).then_some(view! { <Separator /> });

        #[derive(Default)]
        struct InteractionCounters {
            flash: u64,
            send: u64,
            attempt: u64,
        }
        let mut interaction_counters = InteractionCounters::default();
        let interaction_counters_view = {
            let user_ints = ctx.user_interactions.read();
            for problem in ctx.filtered_problems.read().iter() {
                if let Some(user_int) = user_ints.get(problem) {
                    match user_int.best_attempt().map(|da| da.attempt) {
                        Some(models::Attempt::Flash) => interaction_counters.flash += 1,
                        Some(models::Attempt::Send) => interaction_counters.send += 1,
                        Some(models::Attempt::Attempt) => interaction_counters.attempt += 1,
                        None => {}
                    }
                }
            }
            let flash = (interaction_counters.flash > 0).then(|| {
                let class = Gradient::CyanBlue.class_text();
                view! {
                    <span class="mx-1">
                        <span class=class>{interaction_counters.flash}</span>
                    </span>
                }
            });
            let send = (interaction_counters.send > 0).then(|| {
                let class = Gradient::TealLime.class_text();
                view! {
                    <span class="mx-1">
                        <span class=class>{interaction_counters.send}</span>
                    </span>
                }
            });
            let attempt = (interaction_counters.attempt > 0).then(|| {
                let class = Gradient::PinkOrange.class_text();
                view! {
                    <span class="mx-1">
                        <span class=class>{interaction_counters.attempt}</span>
                    </span>
                }
            });

            if flash.is_some() || send.is_some() || attempt.is_some() {
                view! {
                    <span>{"("}</span>
                    {flash}
                    {send}
                    {attempt}
                    <span>{")"}</span>
                }
                .into_any()
            } else {
                ().into_any()
            }
        };

        view! {
            <div class="grid grid-cols-5 gap-1">{cells}</div>
            {sep}
            {problems_counter}
            {interaction_counters_view}
        }
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn AttemptRadioGroup() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    let problem = ctx.problem;

    let mut attempt_radio_buttons = vec![];
    for variant in [models::Attempt::Flash, models::Attempt::Send, models::Attempt::Attempt] {
        let ui_toggle = Signal::derive(move || ctx.todays_attempt.get() == Some(variant));

        let onclick = move |_| {
            let attempt = if ui_toggle.get() { None } else { Some(variant) };

            if let Some(problem) = problem.get() {
                ctx.cb_upsert_todays_attempt.run(server_functions::UpsertTodaysAttempt {
                    wall_uid: ctx.wall.read().uid,
                    problem,
                    attempt,
                });
            }
        };
        attempt_radio_buttons.push(view! { <AttemptRadioButton on:click=onclick variant selected=ui_toggle /> });
    }

    view! { <div class="flex flex-col gap-2 justify-evenly 2xl:flex-row">{attempt_radio_buttons}</div> }
}

#[component]
#[tracing::instrument(skip_all)]
fn AttemptRadioButton(variant: models::Attempt, #[prop(into)] selected: Signal<bool>) -> impl IntoView {
    crate::tracing::on_enter!();

    let text = variant.to_string();
    let icon = variant.icon();
    let color = match variant {
        models::Attempt::Attempt => Gradient::PinkOrange,
        models::Attempt::Send => Gradient::TealLime,
        models::Attempt::Flash => Gradient::CyanBlue,
    };
    view! { <Button text icon color highlight=selected /> }
}

#[component]
#[tracing::instrument(skip_all)]
fn History() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    let attempts = move || {
        ctx.user_interaction
            .read()
            .as_ref()
            .iter()
            .flat_map(|x| x.attempted_on())
            .map(|dated_attempt| {
                let date = dated_attempt.date_time;
                let attempt = dated_attempt.attempt;
                view! { <Attempt date attempt /> }
            })
            .collect_view()
    };

    let placeholder = move || {
        ctx.latest_attempt.read().is_none().then(|| {
            let today = chrono::Utc::now();
            view! { <Attempt date=today attempt=None /> }
        })
    };

    view! {
        {placeholder}
        {attempts}
    }
}

#[component]
#[tracing::instrument(skip_all)]
fn Wall() -> impl IntoView {
    crate::tracing::on_enter!();

    let ctx = use_context::<Context>().unwrap();

    move || {
        let wall = ctx.wall.read();

        let mut cells = vec![];
        for (&hold_position, hold) in &wall.holds {
            let hold_role = signals::hold_role(ctx.problem, hold_position);

            let on_click = move |_| {
                ctx.cb_click_hold.run(hold_position);
            };

            let cell = view! {
                <div class="cursor-pointer">
                    <Hold on:click=on_click role=hold_role hold=hold.clone() />
                </div>
            };
            cells.push(cell);
        }

        let style = {
            let grid_rows = crate::css::grid_rows_n(wall.wall_dimensions.rows);
            let grid_cols = crate::css::grid_cols_n(wall.wall_dimensions.cols);
            let max_width = format!("{}vh", wall.wall_dimensions.cols as f64 / wall.wall_dimensions.rows as f64 * 100.);
            format!("max-height: 100vh; max-width: {max_width}; {}", [grid_rows, grid_cols].join(" "))
        };

        view! {
            <div style=style class="grid gap-1 p-1">
                {cells}
            </div>
        }
    }
}

// TODO: refactor this to use the Problem component
#[component]
#[tracing::instrument(skip_all)]
fn Hold(
    hold: models::Hold,

    #[prop(optional)]
    #[prop(into)]
    role: Option<Signal<Option<HoldRole>>>,
) -> impl IntoView {
    crate::tracing::on_enter!();

    move || {
        let mut class = "bg-sky-100 max-w-full max-h-full aspect-square rounded-sm hover:brightness-125".to_string();
        if let Some(role) = role {
            let role = role.get();

            let role_classes = match role {
                Some(HoldRole::Start) => Some("outline outline-3 outline-green-500"),
                Some(HoldRole::Normal) => Some("outline outline-3 outline-blue-500"),
                Some(HoldRole::Zone) => Some("outline outline-3 outline-amber-500"),
                Some(HoldRole::End) => Some("outline outline-3 outline-red-500"),
                None => Some("brightness-50"),
            };
            if let Some(c) = role_classes {
                class.push(' ');
                class.push_str(c);
            }
        }

        let img = hold.image.as_ref().map(|img| {
            let srcset = img.srcset();
            view! { <img class="object-cover w-full h-full" srcset=srcset /> }
        });

        view! { <div class=class>{img}</div> }
    }
}

#[component]
fn Separator() -> impl IntoView {
    view! { <div class="m-2 h-4 sm:m-3 md:m-4" /> }
}

#[component]
fn Section(children: Children, #[prop(into)] title: MaybeProp<String>) -> impl IntoView {
    view! {
        <div class="px-5 pt-3 pb-8 rounded-lg bg-neutral-900">
            <div class="py-3 text-lg text-center text-orange-400 border-t border-orange-400">
                {move || title.get()}
            </div>
            {children()}
        </div>
    }
}

mod signals {
    use crate::models;
    use leptos::prelude::*;
    use std::collections::BTreeMap;
    use std::collections::BTreeSet;

    pub fn latest_attempt(user_interaction: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::DatedAttempt>> {
        Signal::derive(move || user_interaction.read().as_ref().and_then(models::UserInteraction::latest_attempt))
    }

    pub fn todays_attempt(latest_attempt: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::Attempt>> {
        Signal::derive(move || latest_attempt.read().as_ref().and_then(models::UserInteraction::todays_attempt))
    }

    #[expect(dead_code)]
    pub fn wall_uid(wall: Signal<models::Wall>) -> Signal<models::WallUid> {
        Signal::derive(move || wall.read().uid)
    }

    pub fn user_interaction(
        user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
        problem: Signal<Option<models::Problem>>,
    ) -> Signal<Option<models::UserInteraction>> {
        Signal::derive(move || {
            let problem = problem.get()?;
            let user_interactions = user_interactions.read();
            user_interactions.get(&problem).cloned()
        })
    }

    pub(crate) fn filtered_problems(
        wall: Signal<models::Wall>,
        filter_holds: Signal<BTreeSet<models::HoldPosition>>,
    ) -> Memo<BTreeSet<models::Problem>> {
        Memo::new(move |_prev_val| {
            let filter_holds = filter_holds.read();
            wall.with(|wall| {
                wall.problems
                    .iter()
                    .filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos)))
                    .map(|problem| problem.clone())
                    .collect::<BTreeSet<models::Problem>>()
            })
        })
    }

    pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> {
        Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied()))
    }
}
