use crate::codec::ron::Ron;
use crate::codec::ron::RonEncoded;
use crate::components::StyledHeader;
use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::models;
use crate::models::HoldPosition;
use crate::models::WallUid;
use leptos::Params;
use leptos::ev::Event;
use leptos::html::Input;
use leptos::prelude::*;
use leptos_router::params::Params;
use serde::Deserialize;
use serde::Serialize;
use server_fn::codec::Cbor;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::FileList;

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

#[component]
pub fn EditWall() -> impl IntoView {
    let params = leptos_router::hooks::use_params::<RouteParams>();
    let wall_uid = Signal::derive(move || {
        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 header_items = move || HeaderItems {
        left: vec![HeaderItem {
            text: "← Ascend".to_string(),
            link: Some(format!("/wall/{}", wall_uid.get())),
        }],
        middle: vec![HeaderItem {
            text: "HOLDS".to_string(),
            link: None,
        }],
        right: vec![],
    };

    leptos::view! {
        <div class="min-h-screen min-w-screen bg-slate-900">
            <StyledHeader items=Signal::derive(header_items) />

            <div class="container mx-auto mt-2">
                <Suspense fallback=move || {
                    view! { <p>"Loading..."</p> }
                }>
                    {move || Suspend::new(async move {
                        let wall = wall.await;
                        view! {
                            <ErrorBoundary fallback=|_errors| {
                                "error"
                            }>
                                {move || -> Result<_, ServerFnError> {
                                    let wall = wall.clone()?;
                                    Ok(view! { <Ready wall /> })
                                }}
                            </ErrorBoundary>
                        }
                    })}
                </Suspense>
            </div>
        </div>
    }
}

#[component]
fn Ready(wall: models::Wall) -> impl IntoView {
    tracing::debug!("ready");

    let mut holds = vec![];
    for hold in wall.holds.values().cloned() {
        holds.push(view! { <Hold wall_uid=wall.uid hold /> });
    }

    let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", wall.rows, wall.cols);

    view! {
        <div>
            <p class="my-4 font-semibold">"Click hold to replace image"</p>
            <div class=move || { grid_classes.clone() }>{holds}</div>
        </div>
    }
}

#[component]
fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView {
    let hold_position = hold.position;
    let file_input_ref = NodeRef::<Input>::new();

    let open_camera = move |_| {
        if let Some(input) = file_input_ref.get() {
            input.click(); // Trigger the file input click programmatically
        }
    };

    let upload = ServerAction::<SetImage>::new();

    let hold = Signal::derive(move || {
        let refreshed = upload.value().get().map(Result::unwrap);
        refreshed.map(RonEncoded::into_inner).unwrap_or(hold.clone())
    });

    // Callback to handle file selection
    let on_file_input = move |event: Event| {
        let files: FileList = event.target().unwrap().unchecked_ref::<web_sys::HtmlInputElement>().files().unwrap();
        let file = files.item(0).unwrap();

        let file_reader = web_sys::FileReader::new().unwrap();
        file_reader.read_as_array_buffer(&file).unwrap();

        let on_load = Closure::wrap(Box::new(move |event: Event| {
            let file_reader: web_sys::FileReader = event.target().unwrap().dyn_into().unwrap();
            let file = file_reader.result().unwrap();
            let file = web_sys::js_sys::Uint8Array::new(&file);

            let mut file_contents = vec![0; file.length() as usize];
            file.copy_to(&mut file_contents);

            tracing::debug!("bytes: {:?}", &file_contents.len());

            let image = Image {
                file_name: "foo".to_string(),
                file_contents,
            };

            upload.dispatch(SetImage {
                wall_uid,
                hold_position,
                image,
            });
        }) as Box<dyn FnMut(_)>);

        file_reader.set_onload(Some(on_load.as_ref().unchecked_ref()));
        on_load.forget();
    };

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

    view! {
        <button on:click=open_camera>
            <div class="bg-indigo-100 rounded aspect-square">{img}</div>
        </button>

        <input
            node_ref=file_input_ref
            type="file"
            accept="image/*"
            capture="user"
            style="display: none;"
            on:input=on_file_input
        />
    }
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Image {
    file_name: String,
    file_contents: Vec<u8>,
}

#[server(
    name = SetImage,
    input = Cbor,
    output = Ron,
)]
#[tracing::instrument(skip(image), err)]
async fn set_image(wall_uid: WallUid, hold_position: HoldPosition, image: Image) -> Result<RonEncoded<models::Hold>, ServerFnError> {
    use image::ImageDecoder;
    use std::collections::BTreeMap;
    use std::path::Path;

    tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len());

    let db = expect_context::<crate::server::db::Database>();

    let image = tokio::task::spawn_blocking(move || -> Result<models::Image, ServerFnError> {
        let mut decoder = image::ImageReader::new(std::io::Cursor::new(image.file_contents))
            .with_guessed_format()?
            .into_decoder()?;
        let orientation = decoder.orientation()?;
        let mut img = image::DynamicImage::from_decoder(decoder)?;
        img.apply_orientation(orientation);

        let holds_dir = Path::new("datastore/public/holds");
        std::fs::create_dir_all(holds_dir)?;

        let targets = [(50, 50), (150, 150), (300, 300), (400, 400)];

        let uid = models::ImageUid::create();
        let mut resolutions = BTreeMap::new();
        for (width, height) in targets {
            let resized = img.resize_to_fill(width, height, image::imageops::FilterType::Lanczos3);

            let filename = format!("hold_row{}_col{}_{width}x{height}_{uid}.webp", hold_position.row, hold_position.col);
            let path = holds_dir.join(&filename);
            let mut file = std::fs::OpenOptions::new().write(true).append(false).create_new(true).open(&path)?;
            resized.write_to(&mut file, image::ImageFormat::WebP)?;

            let res = models::ImageResolution {
                width: width.into(),
                height: height.into(),
            };
            resolutions.insert(res, models::ImageFilename { filename });
        }

        // TODO: Clean up old image?

        Ok(models::Image { uid, resolutions })
    })
    .await??;

    let hold = db
        .write(move |txn| {
            use redb::ReadableTable;
            let mut walls = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
            let mut wall = walls.get(wall_uid)?.expect("todo").value();

            let hold = wall.holds.get_mut(&hold_position).expect("hold");
            hold.image = Some(image);
            let hold = hold.clone();
            walls.insert(wall_uid, wall)?;

            Ok(hold)
        })
        .await?;

    Ok(RonEncoded::new(hold))
}
