use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Redirect, Response};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Deserializer};
use tracing::info;

use crate::application::auth::AuthenticatedUser;
use crate::application::errors::{ApiError, AppError};
use crate::application::routes::api::images::save_deferred_image;
use crate::application::routes::api::macros::{define_delete_handler, define_enriched_get_handler};
use crate::application::routes::support::{
    FlexiblePayload, ListQuery, PayloadSource, impl_has_changes, is_datastar_request,
    validate_update,
};
use crate::application::state::AppState;
use crate::domain::bags::BagFilter;
use crate::domain::brews::{
    BrewFilter, BrewSortKey, BrewWithDetails, NewBrew, QuickNote, UpdateBrew,
};
use crate::domain::gear::{GearCategory, GearFilter, GearSortKey};
use crate::domain::ids::{BagId, BrewId, GearId};
use crate::domain::images::ImageData;
use crate::domain::listing::{ListRequest, PageSize, SortDirection};
use crate::presentation::web::templates::BrewListTemplate;
use crate::presentation::web::views::{
    BagOptionView, BrewDefaultsView, BrewView, GearOptionView, ListNavigator, Paginated,
    QuickNoteView,
};

const BREW_PAGE_PATH: &str = "/data?type=brews";
const BREW_FRAGMENT_PATH: &str = "/data?type=brews#brew-list";

pub(crate) struct BrewPageData {
    pub(crate) brews: Paginated<BrewView>,
    pub(crate) navigator: ListNavigator<BrewSortKey>,
}

pub(crate) struct BrewFormData {
    pub(crate) bag_options: Vec<BagOptionView>,
    pub(crate) grinder_options: Vec<GearOptionView>,
    pub(crate) brewer_options: Vec<GearOptionView>,
    pub(crate) filter_paper_options: Vec<GearOptionView>,
    pub(crate) defaults: BrewDefaultsView,
    pub(crate) quick_note_options: Vec<QuickNoteView>,
}

pub(crate) async fn load_brew_form_data(state: &AppState) -> Result<BrewFormData, AppError> {
    let open_bags_request = ListRequest::show_all(
        crate::domain::bags::BagSortKey::RoastDate,
        SortDirection::Desc,
    );
    let open_bags = state
        .bag_repo
        .list(BagFilter::open(), &open_bags_request, None)
        .await
        .map_err(AppError::from)?;
    let bag_options: Vec<BagOptionView> = open_bags
        .items
        .into_iter()
        .map(BagOptionView::from)
        .collect();

    let gear_request = ListRequest::show_all(GearSortKey::Make, SortDirection::Asc);

    let grinder_options = load_gear_options(state, GearCategory::Grinder, &gear_request).await?;
    let brewer_options = load_gear_options(state, GearCategory::Brewer, &gear_request).await?;
    let filter_paper_options =
        load_gear_options(state, GearCategory::FilterPaper, &gear_request).await?;

    let last_brew_request = ListRequest::new(
        1,
        PageSize::Limited(1),
        BrewSortKey::CreatedAt,
        SortDirection::Desc,
    );
    let last_brew_page = state
        .brew_repo
        .list(BrewFilter::all(), &last_brew_request, None)
        .await
        .map_err(AppError::from)?;
    let defaults = last_brew_page
        .items
        .into_iter()
        .next()
        .map(BrewDefaultsView::from)
        .unwrap_or_default();

    let quick_note_options = QuickNote::all()
        .iter()
        .copied()
        .map(QuickNoteView::from)
        .collect();

    Ok(BrewFormData {
        bag_options,
        grinder_options,
        brewer_options,
        filter_paper_options,
        defaults,
        quick_note_options,
    })
}

pub(crate) async fn load_gear_options(
    state: &AppState,
    category: GearCategory,
    request: &ListRequest<GearSortKey>,
) -> Result<Vec<GearOptionView>, AppError> {
    let page = state
        .gear_repo
        .list(GearFilter::for_category(category), request, None)
        .await
        .map_err(AppError::from)?;
    Ok(page.items.into_iter().map(GearOptionView::from).collect())
}

#[tracing::instrument(skip(state))]
pub(crate) async fn load_brew_page(
    state: &AppState,
    request: ListRequest<BrewSortKey>,
    search: Option<&str>,
) -> Result<BrewPageData, AppError> {
    let page = state
        .brew_repo
        .list(BrewFilter::all(), &request, search)
        .await
        .map_err(AppError::from)?;

    let (brews, navigator) = crate::application::routes::support::build_page_view(
        page,
        request,
        BrewView::from_domain,
        BREW_PAGE_PATH,
        BREW_FRAGMENT_PATH,
        search.map(String::from),
    );

    Ok(BrewPageData { brews, navigator })
}

/// Deserializes an optional `GearId`, treating empty strings (from HTML forms) as None.
fn deserialize_optional_gear_id<'de, D>(deserializer: D) -> Result<Option<GearId>, D::Error>
where
    D: Deserializer<'de>,
{
    let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
    match value {
        None | Some(serde_json::Value::Null) => Ok(None),
        Some(serde_json::Value::String(s)) if s.is_empty() => Ok(None),
        Some(serde_json::Value::Number(n)) => n
            .as_i64()
            .map(|id| Some(GearId::new(id)))
            .ok_or_else(|| serde::de::Error::custom("invalid gear id")),
        Some(serde_json::Value::String(s)) => s
            .parse::<i64>()
            .map(|id| Some(GearId::new(id)))
            .map_err(serde::de::Error::custom),
        Some(_) => Err(serde::de::Error::custom("invalid gear id")),
    }
}

fn deserialize_quick_notes<'de, D>(deserializer: D) -> Result<Vec<QuickNote>, D::Error>
where
    D: Deserializer<'de>,
{
    let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
    match value {
        None | Some(serde_json::Value::Null) => Ok(Vec::new()),
        Some(serde_json::Value::String(s)) if s.is_empty() => Ok(Vec::new()),
        Some(serde_json::Value::String(s)) => Ok(s
            .split(',')
            .filter_map(|v| QuickNote::from_str_value(v.trim()))
            .collect()),
        Some(serde_json::Value::Array(arr)) => Ok(arr
            .iter()
            .filter_map(|v| v.as_str().and_then(QuickNote::from_str_value))
            .collect()),
        Some(_) => Err(serde::de::Error::custom("invalid quick_notes")),
    }
}

#[derive(Debug, Deserialize)]
pub(crate) struct NewBrewSubmission {
    bag_id: BagId,
    coffee_weight: f64,
    grinder_id: GearId,
    grind_setting: f64,
    brewer_id: GearId,
    #[serde(default, deserialize_with = "deserialize_optional_gear_id")]
    filter_paper_id: Option<GearId>,
    water_volume: i32,
    water_temp: f64,
    #[serde(default, deserialize_with = "deserialize_quick_notes")]
    quick_notes: Vec<QuickNote>,
    #[serde(default)]
    brew_time: Option<i32>,
    #[serde(default)]
    created_at: Option<DateTime<Utc>>,
    #[serde(default)]
    image: ImageData,
}

impl NewBrewSubmission {
    fn into_parts(self) -> Result<(NewBrew, Option<String>), AppError> {
        if self.coffee_weight <= 0.0 {
            return Err(AppError::validation("coffee weight must be positive"));
        }
        if self.grind_setting < 0.0 {
            return Err(AppError::validation("grind setting must be non-negative"));
        }
        if self.water_volume <= 0 {
            return Err(AppError::validation("water volume must be positive"));
        }
        if self.water_temp <= 0.0 || self.water_temp > 100.0 {
            return Err(AppError::validation(
                "water temperature must be between 0 and 100",
            ));
        }
        if let Some(bt) = self.brew_time
            && bt <= 0
        {
            return Err(AppError::validation("brew time must be positive"));
        }

        Ok((
            NewBrew {
                bag_id: self.bag_id,
                coffee_weight: self.coffee_weight,
                grinder_id: self.grinder_id,
                grind_setting: self.grind_setting,
                brewer_id: self.brewer_id,
                filter_paper_id: self.filter_paper_id,
                water_volume: self.water_volume,
                water_temp: self.water_temp,
                quick_notes: self.quick_notes,
                brew_time: self.brew_time,
                created_at: self.created_at,
            },
            self.image.into_inner(),
        ))
    }
}

#[tracing::instrument(skip(state, _auth_user, headers, query))]
pub(crate) async fn create_brew(
    State(state): State<AppState>,
    _auth_user: AuthenticatedUser,
    headers: HeaderMap,
    Query(query): Query<ListQuery>,
    payload: FlexiblePayload<NewBrewSubmission>,
) -> Result<Response, ApiError> {
    let (request, search) = query.into_request_and_search::<BrewSortKey>();
    let (submission, source) = payload.into_parts();
    let (new_brew, image_data_url) = submission.into_parts().map_err(ApiError::from)?;

    let enriched = state
        .brew_service
        .create(new_brew)
        .await
        .map_err(AppError::from)?;

    info!(brew_id = %enriched.brew.id, "brew created");
    state.stats_invalidator.invalidate();

    save_deferred_image(
        &state,
        "brew",
        i64::from(enriched.brew.id),
        image_data_url.as_deref(),
    )
    .await;

    let detail_url = format!("/brews/{}", enriched.brew.id);

    if is_datastar_request(&headers) {
        // If the request came from a page that has #brew-list, return the updated fragment.
        // Otherwise (homepage, add page, etc.), redirect to the brew detail page.
        let from_brew_page = headers
            .get("referer")
            .and_then(|v| v.to_str().ok())
            .is_some_and(|r| r.contains("type=brews"));

        if from_brew_page {
            render_brew_list_fragment(state, request, search, true)
                .await
                .map_err(ApiError::from)
        } else {
            crate::application::routes::support::render_redirect_script(&detail_url)
                .map_err(ApiError::from)
        }
    } else if matches!(source, PayloadSource::Form) {
        Ok(Redirect::to(&detail_url).into_response())
    } else {
        Ok((StatusCode::CREATED, Json(enriched)).into_response())
    }
}

#[derive(Debug, Deserialize)]
pub struct BrewsQuery {
    pub bag_id: Option<BagId>,
}

#[tracing::instrument(skip(state))]
pub(crate) async fn list_brews(
    State(state): State<AppState>,
    Query(params): Query<BrewsQuery>,
) -> Result<Json<Vec<BrewWithDetails>>, ApiError> {
    let filter = match params.bag_id {
        Some(bag_id) => BrewFilter::for_bag(bag_id),
        None => BrewFilter::all(),
    };
    let request = ListRequest::show_all(BrewSortKey::CreatedAt, SortDirection::Desc);
    let page = state
        .brew_repo
        .list(filter, &request, None)
        .await
        .map_err(AppError::from)?;
    Ok(Json(page.items))
}

define_enriched_get_handler!(
    get_brew,
    BrewId,
    BrewWithDetails,
    brew_repo,
    get_with_details
);

#[derive(Debug, Deserialize)]
pub(crate) struct UpdateBrewSubmission {
    #[serde(default)]
    bag_id: Option<BagId>,
    #[serde(default)]
    coffee_weight: Option<f64>,
    #[serde(default, deserialize_with = "deserialize_optional_gear_id")]
    grinder_id: Option<GearId>,
    #[serde(default)]
    grind_setting: Option<f64>,
    #[serde(default, deserialize_with = "deserialize_optional_gear_id")]
    brewer_id: Option<GearId>,
    #[serde(default, deserialize_with = "deserialize_optional_gear_id")]
    filter_paper_id: Option<GearId>,
    #[serde(default)]
    water_volume: Option<i32>,
    #[serde(default)]
    water_temp: Option<f64>,
    #[serde(default, deserialize_with = "deserialize_quick_notes")]
    quick_notes: Vec<QuickNote>,
    #[serde(default)]
    brew_time: Option<i32>,
    #[serde(default)]
    created_at: Option<DateTime<Utc>>,
    #[serde(default)]
    image: ImageData,
}

impl UpdateBrewSubmission {
    fn into_parts(self) -> (UpdateBrew, Option<String>) {
        let update = UpdateBrew {
            bag_id: self.bag_id,
            coffee_weight: self.coffee_weight,
            grinder_id: self.grinder_id,
            grind_setting: self.grind_setting,
            brewer_id: self.brewer_id,
            filter_paper_id: self.filter_paper_id,
            water_volume: self.water_volume,
            water_temp: self.water_temp,
            quick_notes: if self.quick_notes.is_empty() {
                None
            } else {
                Some(self.quick_notes)
            },
            brew_time: self.brew_time,
            created_at: self.created_at,
        };
        (update, self.image.into_inner())
    }
}

impl_has_changes!(
    UpdateBrew,
    bag_id,
    coffee_weight,
    grinder_id,
    grind_setting,
    brewer_id,
    filter_paper_id,
    water_volume,
    water_temp,
    quick_notes,
    brew_time,
    created_at
);

#[tracing::instrument(skip(state, _auth_user, headers))]
pub(crate) async fn update_brew(
    State(state): State<AppState>,
    _auth_user: AuthenticatedUser,
    headers: HeaderMap,
    Path(id): Path<BrewId>,
    payload: FlexiblePayload<UpdateBrewSubmission>,
) -> Result<Response, ApiError> {
    let (submission, source) = payload.into_parts();
    let (update, image_data_url) = submission.into_parts();

    validate_update(&update, image_data_url.as_ref())?;

    state
        .brew_repo
        .update(id, update)
        .await
        .map_err(AppError::from)?;

    info!(%id, "brew updated");
    state.stats_invalidator.invalidate();

    save_deferred_image(&state, "brew", i64::from(id), image_data_url.as_deref()).await;

    let detail_url = format!("/brews/{id}");

    if is_datastar_request(&headers) {
        crate::application::routes::support::render_redirect_script(&detail_url)
            .map_err(ApiError::from)
    } else if matches!(source, PayloadSource::Form) {
        Ok(Redirect::to(&detail_url).into_response())
    } else {
        let enriched = state
            .brew_repo
            .get_with_details(id)
            .await
            .map_err(AppError::from)?;
        Ok(Json(enriched).into_response())
    }
}

define_delete_handler!(
    delete_brew,
    BrewId,
    BrewSortKey,
    brew_repo,
    render_brew_list_fragment,
    "type=brews",
    "/data?type=brews",
    image_type: "brew"
);

async fn render_brew_list_fragment(
    state: AppState,
    request: ListRequest<BrewSortKey>,
    search: Option<String>,
    is_authenticated: bool,
) -> Result<Response, AppError> {
    let BrewPageData { brews, navigator } =
        load_brew_page(&state, request, search.as_deref()).await?;

    let template = BrewListTemplate {
        is_authenticated,
        brews,
        navigator,
    };

    crate::application::routes::support::render_fragment(template, "#brew-list")
}
