//! The one TMDB matcher UI shared by movies and shows. Generic only over the
//! match payload `M`; which service functions it drives comes from
//! [`Backend`]. Emits its own [`Message`] — owners map it into their domain
//! message via `Task::map` / `Element::map`.

use crate::tmdb_service::{self, TmdbSearchItem};
use crate::ui::shared::styles::{self, GRAY_40, GRAY_60, GRAY_70, GRAY_80};
use iced::{Element, Task};

const RESULT_POSTER_WIDTH: f32 = 80.0;
const RESULT_POSTER_HEIGHT: f32 = 120.0;

#[derive(Debug, Clone)]
pub(crate) enum Message<M> {
    ToggleSearch,
    QueryChanged(String),
    Submit,
    SearchCompleted(i64, Result<Vec<TmdbSearchItem>, String>),
    SelectMatch(usize),
    /// Intercepted by the owning domain module (needs `&mut Database` and the
    /// library items); never handled in [`update`].
    MatchCompleted(i64, Result<M, String>),
}

/// Per-domain plumbing: which service functions the matcher drives.
pub(crate) struct Backend<M> {
    pub(crate) search: fn(String, String) -> Result<Vec<TmdbSearchItem>, String>,
    pub(crate) fetch_match: fn(String, TmdbSearchItem) -> Result<M, String>,
}

// Manual impls: derive would demand `M: Copy`/`M: Clone`, but only fn
// pointers are stored.
impl<M> Copy for Backend<M> {}
impl<M> Clone for Backend<M> {
    fn clone(&self) -> Self {
        *self
    }
}

pub(crate) struct MatcherState {
    pub(crate) search_query: String,
    pub(crate) search_results: Vec<TmdbSearchItem>,
    pub(crate) searching: bool,
    pub(crate) matching: bool,
    pub(crate) search_expanded: bool,
    pub(crate) error: Option<String>,
    placeholder: &'static str,
    matching_text: &'static str,
}

impl MatcherState {
    pub(crate) fn new(
        title: &str,
        has_tmdb_id: bool,
        placeholder: &'static str,
        matching_text: &'static str,
    ) -> Self {
        Self {
            search_query: title.to_string(),
            search_results: Vec::new(),
            searching: false,
            matching: false,
            search_expanded: !has_tmdb_id,
            error: None,
            placeholder,
            matching_text,
        }
    }

    /// Collapse the search section after a successful match.
    pub(crate) fn match_succeeded(&mut self) {
        self.matching = false;
        self.search_expanded = false;
        self.search_results.clear();
    }

    pub(crate) fn match_failed(&mut self, error: String) {
        self.matching = false;
        self.error = Some(error);
    }
}

/// `target_id` is the database id of the movie/show this matcher is open
/// for; it is embedded into the completion messages so late results can
/// never be applied to a different item.
pub(crate) fn update<M: Send + 'static>(
    state: &mut MatcherState,
    msg: Message<M>,
    tmdb_api_token: &Option<String>,
    target_id: i64,
    backend: Backend<M>,
) -> Task<Message<M>> {
    match msg {
        Message::ToggleSearch => {
            state.search_expanded = !state.search_expanded;
            Task::none()
        }
        Message::QueryChanged(text) => {
            state.search_query = text;
            Task::none()
        }
        Message::Submit => {
            if state.search_query.trim().is_empty() {
                return Task::none();
            }
            state.searching = true;
            state.search_results.clear();
            state.error = None;

            let token = tmdb_api_token.clone().unwrap_or_default();
            let query = state.search_query.clone();
            let search = backend.search;

            Task::perform(
                tmdb_service::run_blocking(move || search(token, query)),
                move |result| Message::SearchCompleted(target_id, result),
            )
        }
        Message::SearchCompleted(id, result) => {
            if id != target_id {
                // Stale result for an item the user has navigated away from.
                return Task::none();
            }
            state.searching = false;
            match result {
                Ok(results) => state.search_results = results,
                Err(e) => state.error = Some(e),
            }
            Task::none()
        }
        Message::SelectMatch(idx) => {
            let Some(item) = state.search_results.get(idx).cloned() else {
                return Task::none();
            };
            state.matching = true;
            state.error = None;

            let token = tmdb_api_token.clone().unwrap_or_default();
            let fetch = backend.fetch_match;

            Task::perform(
                tmdb_service::run_blocking(move || fetch(token, item)),
                move |result| Message::MatchCompleted(target_id, result),
            )
        }
        // Handled by the owning domain module before delegation.
        Message::MatchCompleted(..) => Task::none(),
    }
}

pub(crate) fn view<M: Clone + 'static>(state: &MatcherState) -> Element<'_, Message<M>> {
    let mut section = iced::widget::column![].spacing(styles::SPACING_MEDIUM);

    if !state.search_expanded {
        let toggle_btn = iced::widget::button(
            iced::widget::text("Change TMDB match")
                .size(14)
                .color(GRAY_70),
        )
        .on_press(Message::ToggleSearch)
        .padding(iced::Padding::from([6.0, 16.0]))
        .style(styles::tmdb_toggle_button_style);

        return section.push(toggle_btn).into();
    }

    let label = iced::widget::text("TMDB Search").size(18).color(GRAY_80);
    section = section.push(label);

    if state.matching {
        section = section.push(
            iced::widget::text(state.matching_text)
                .size(14)
                .color(GRAY_60),
        );
        return section.into();
    }

    let search_input = iced::widget::text_input(state.placeholder, &state.search_query)
        .on_input(Message::QueryChanged)
        .on_submit(Message::Submit)
        .size(14)
        .padding(8)
        .width(400);

    let mut search_btn = iced::widget::button(
        iced::widget::text("Search")
            .size(14)
            .color(iced::Color::WHITE),
    )
    .padding(iced::Padding::from([8.0, 16.0]))
    .style(styles::blue_button_style);

    if !state.searching && !state.search_query.trim().is_empty() {
        search_btn = search_btn.on_press(Message::Submit);
    }

    let search_row = iced::widget::row![search_input, search_btn].spacing(styles::SPACING_DEFAULT);
    section = section.push(search_row);

    if state.searching {
        section = section.push(iced::widget::text("Searching...").size(14).color(GRAY_60));
    }

    if let Some(error) = &state.error {
        section = section.push(iced::widget::text(error).size(14).color(styles::ERROR_RED));
    }

    for (idx, result) in state.search_results.iter().enumerate() {
        section = section.push(result_row(idx, result));
    }

    section.into()
}

fn result_row<M: Clone + 'static>(idx: usize, result: &TmdbSearchItem) -> Element<'_, Message<M>> {
    let poster: Element<_> = if let Some(bytes) = &result.poster_image {
        iced::widget::image(iced::widget::image::Handle::from_bytes(bytes.clone()))
            .width(RESULT_POSTER_WIDTH)
            .height(RESULT_POSTER_HEIGHT)
            .into()
    } else {
        iced::widget::container(iced::widget::text("?").size(20).color(GRAY_40))
            .width(RESULT_POSTER_WIDTH)
            .height(RESULT_POSTER_HEIGHT)
            .center_x(RESULT_POSTER_WIDTH)
            .center_y(RESULT_POSTER_HEIGHT)
            .style(styles::placeholder_style)
            .into()
    };

    let info = iced::widget::column![
        iced::widget::text(&result.title)
            .size(16)
            .color(iced::Color::WHITE),
        iced::widget::text(result.year_str())
            .size(13)
            .color(GRAY_60),
    ]
    .spacing(styles::SPACING_TIGHT);

    let select_btn = iced::widget::button(
        iced::widget::text("Select")
            .size(13)
            .color(iced::Color::WHITE),
    )
    .on_press(Message::SelectMatch(idx))
    .padding(iced::Padding::from([6.0, 14.0]))
    .style(styles::green_button_style);

    let row = iced::widget::row![
        poster,
        info.width(iced::Length::Fill),
        iced::widget::container(select_btn).center_y(RESULT_POSTER_HEIGHT),
    ]
    .spacing(styles::SPACING_MEDIUM)
    .align_y(iced::Alignment::Start);

    iced::widget::container(row)
        .padding(8)
        .style(styles::result_row_style)
        .into()
}
