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;

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, define_list_fragment_renderer,
};
use crate::application::routes::support::{
    FlexiblePayload, ListQuery, PayloadSource, impl_has_changes, is_datastar_request,
    render_redirect_script, validate_update,
};
use crate::application::state::AppState;
use crate::domain::ids::{RoastId, RoasterId};
use crate::domain::images::ImageData;
use crate::domain::listing::{ListRequest, SortDirection};
use crate::domain::roasts::{NewRoast, RoastSortKey, RoastWithRoaster, UpdateRoast};
use crate::infrastructure::ai::{self, ExtractionInput};
use crate::presentation::web::templates::{RoastListTemplate, RoastOptionsTemplate};
use crate::presentation::web::views::{ListNavigator, Paginated, RoastView};
use tracing::info;

const ROAST_PAGE_PATH: &str = "/data?type=roasts";
const ROAST_FRAGMENT_PATH: &str = "/data?type=roasts#roast-list";

#[tracing::instrument(skip(state))]
pub(crate) async fn load_roast_page(
    state: &AppState,
    request: ListRequest<RoastSortKey>,
    search: Option<&str>,
) -> Result<(Paginated<RoastView>, ListNavigator<RoastSortKey>), AppError> {
    let page = state
        .roast_repo
        .list(&request, search)
        .await
        .map_err(AppError::from)?;

    Ok(crate::application::routes::support::build_page_view(
        page,
        request,
        RoastView::from_list_item,
        ROAST_PAGE_PATH,
        ROAST_FRAGMENT_PATH,
        search.map(String::from),
    ))
}

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

    state
        .roaster_repo
        .get(new_roast.roaster_id)
        .await
        .map_err(|err| ApiError::from(AppError::from(err)))?;

    let roast = state
        .roast_service
        .create(new_roast)
        .await
        .map_err(AppError::from)?;

    info!(roast_id = %roast.id, name = %roast.name, "roast created");
    state.stats_invalidator.invalidate();

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

    let roaster = state
        .roaster_repo
        .get(roast.roaster_id)
        .await
        .map_err(AppError::from)?;
    let detail_url = format!("/roasters/{}/roasts/{}", roaster.slug, roast.slug);

    if is_datastar_request(&headers) {
        let from_data_page = headers
            .get("referer")
            .and_then(|v| v.to_str().ok())
            .is_some_and(|r| r.contains("type=roasts"));

        if from_data_page {
            render_roast_list_fragment(state, request, search, true)
                .await
                .map_err(ApiError::from)
        } else {
            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
            .roast_repo
            .get_with_roaster(roast.id)
            .await
            .map_err(AppError::from)?;
        Ok((StatusCode::CREATED, Json(enriched)).into_response())
    }
}

#[tracing::instrument(skip(state, headers))]
pub(crate) async fn list_roasts(
    State(state): State<AppState>,
    headers: HeaderMap,
    Query(params): Query<RoastsQuery>,
) -> Result<Response, ApiError> {
    let roaster_id = match params.roaster_id.as_deref() {
        Some(s) if !s.is_empty() => {
            let s = s.trim();
            Some(s.parse::<RoasterId>().map_err(|_| {
                tracing::warn!("Invalid roaster_id: '{}'", s);
                ApiError::from(AppError::validation(format!("Invalid roaster_id: '{s}'")))
            })?)
        }
        _ => None,
    };

    let roasts = match roaster_id {
        Some(roaster_id) => state
            .roast_repo
            .list_by_roaster(roaster_id)
            .await
            .map_err(AppError::from)?,
        None => {
            if is_datastar_request(&headers) {
                vec![]
            } else {
                state.roast_repo.list_all().await.map_err(AppError::from)?
            }
        }
    };

    if is_datastar_request(&headers) {
        let template = RoastOptionsTemplate { roasts };
        crate::application::routes::support::render_fragment(template, "#roast-select-options")
            .map_err(ApiError::from)
    } else {
        Ok(Json(roasts).into_response())
    }
}

define_enriched_get_handler!(
    get_roast,
    RoastId,
    RoastWithRoaster,
    roast_repo,
    get_with_roaster
);

define_delete_handler!(
    delete_roast,
    RoastId,
    RoastSortKey,
    roast_repo,
    render_roast_list_fragment,
    "type=roasts",
    "/data?type=roasts",
    image_type: "roast"
);

#[derive(Debug, Deserialize)]
pub(crate) struct UpdateRoastSubmission {
    #[serde(default)]
    roaster_id: Option<RoasterId>,
    #[serde(default)]
    name: Option<String>,
    #[serde(default)]
    origin: Option<String>,
    #[serde(default)]
    region: Option<String>,
    #[serde(default)]
    producer: Option<String>,
    #[serde(default)]
    tasting_notes: Option<TastingNotesInput>,
    #[serde(default)]
    process: Option<String>,
    #[serde(default)]
    created_at: Option<DateTime<Utc>>,
    #[serde(default)]
    image: ImageData,
}

impl UpdateRoastSubmission {
    fn into_parts(self) -> (UpdateRoast, Option<String>) {
        let update = UpdateRoast {
            roaster_id: self.roaster_id,
            name: self.name,
            origin: self.origin,
            region: self.region,
            producer: self.producer,
            tasting_notes: self.tasting_notes.map(TastingNotesInput::into_vec),
            process: self.process,
            created_at: self.created_at,
        };
        (update, self.image.into_inner())
    }
}

impl_has_changes!(
    UpdateRoast,
    roaster_id,
    name,
    origin,
    region,
    producer,
    tasting_notes,
    process,
    created_at
);

#[tracing::instrument(skip(state, _auth_user, headers))]
pub(crate) async fn update_roast(
    State(state): State<AppState>,
    _auth_user: AuthenticatedUser,
    headers: HeaderMap,
    Path(id): Path<RoastId>,
    payload: FlexiblePayload<UpdateRoastSubmission>,
) -> 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
        .roast_repo
        .update(id, update)
        .await
        .map_err(AppError::from)?;

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

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

    let enriched = state
        .roast_repo
        .get_with_roaster(id)
        .await
        .map_err(AppError::from)?;

    let detail_url = format!(
        "/roasters/{}/roasts/{}",
        enriched.roaster_slug, enriched.roast.slug
    );

    if is_datastar_request(&headers) {
        render_redirect_script(&detail_url).map_err(ApiError::from)
    } else if matches!(source, PayloadSource::Form) {
        Ok(Redirect::to(&detail_url).into_response())
    } else {
        Ok(Json(enriched).into_response())
    }
}

#[derive(Debug, Deserialize)]
pub struct RoastsQuery {
    pub roaster_id: Option<String>,
}

#[derive(Debug, Deserialize)]
pub(crate) struct NewRoastSubmission {
    roaster_id: RoasterId,
    name: String,
    origin: String,
    region: String,
    producer: String,
    tasting_notes: TastingNotesInput,
    process: String,
    #[serde(default)]
    created_at: Option<DateTime<Utc>>,
    #[serde(default)]
    image: ImageData,
}

impl NewRoastSubmission {
    fn into_parts(self) -> Result<(NewRoast, Option<String>), AppError> {
        fn require(field: &str, value: String) -> Result<String, AppError> {
            let trimmed = value.trim();
            if trimmed.is_empty() {
                Err(AppError::validation(format!("{field} is required")))
            } else {
                Ok(trimmed.to_string())
            }
        }

        let roaster_id = self.roaster_id;
        if roaster_id.into_inner() <= 0 {
            return Err(AppError::validation("invalid roaster id"));
        }
        let name = require("name", self.name)?;
        let origin = require("origin", self.origin)?;
        let region = require("region", self.region)?;
        let producer = require("producer", self.producer)?;
        let process = require("process", self.process)?;

        let tasting_notes = self.tasting_notes.into_vec();

        if tasting_notes.is_empty() {
            return Err(AppError::validation("tasting notes are required"));
        }

        Ok((
            NewRoast {
                roaster_id,
                name,
                origin,
                region,
                producer,
                tasting_notes,
                process,
                created_at: self.created_at,
            },
            self.image.into_inner(),
        ))
    }
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub(crate) enum TastingNotesInput {
    List(Vec<String>),
    Text(String),
}

impl TastingNotesInput {
    pub(crate) fn into_vec(self) -> Vec<String> {
        match self {
            TastingNotesInput::List(values) => values
                .into_iter()
                .map(|value| value.trim().to_string())
                .filter(|value| !value.is_empty())
                .collect(),
            TastingNotesInput::Text(value) => value
                .split([',', '\n'])
                .map(|segment| segment.trim().to_string())
                .filter(|segment| !segment.is_empty())
                .collect(),
        }
    }
}

#[tracing::instrument(skip(state, auth_user, headers, payload))]
pub(crate) async fn extract_roast_info(
    State(state): State<AppState>,
    auth_user: AuthenticatedUser,
    headers: HeaderMap,
    payload: FlexiblePayload<ExtractionInput>,
) -> Result<Response, ApiError> {
    let (input, _) = payload.into_parts();
    let (result, usage) = ai::extract_roast(
        &state.http_client,
        &state.openrouter_url,
        &state.openrouter_api_key,
        &state.openrouter_model,
        &input,
    )
    .await
    .map_err(ApiError::from)?;

    crate::application::routes::support::record_ai_usage(
        state.ai_usage_repo.clone(),
        auth_user.0.id,
        &state.openrouter_model,
        "extract-roast",
        usage,
    );

    if is_datastar_request(&headers) {
        use serde_json::Value;

        // Try to match extracted roaster name to an existing roaster ID
        let roaster_id = if let Some(ref roaster_name) = result.roaster_name {
            match_roaster_id(&state, roaster_name)
                .await
                .unwrap_or_default()
        } else {
            String::new()
        };

        let tasting_notes = result
            .tasting_notes
            .as_ref()
            .map(|notes| notes.join(", "))
            .unwrap_or_default();

        let signals = vec![
            (
                "_roast-name",
                Value::String(result.name.unwrap_or_default()),
            ),
            ("_origin", Value::String(result.origin.unwrap_or_default())),
            ("_region", Value::String(result.region.unwrap_or_default())),
            (
                "_producer",
                Value::String(result.producer.unwrap_or_default()),
            ),
            (
                "_process",
                Value::String(result.process.unwrap_or_default()),
            ),
            ("_tasting-notes", Value::String(tasting_notes)),
            ("_roaster-id", Value::String(roaster_id)),
            ("_extracted", Value::Bool(true)),
        ];
        crate::application::routes::support::render_signals_json(&signals).map_err(ApiError::from)
    } else {
        Ok(Json(result).into_response())
    }
}

/// Fuzzy-match a roaster name against existing roasters and return the matched ID.
async fn match_roaster_id(state: &AppState, roaster_name: &str) -> Option<String> {
    let roasters = state
        .roaster_repo
        .list_all_sorted(
            crate::domain::roasters::RoasterSortKey::Name,
            SortDirection::Asc,
        )
        .await
        .ok()?;

    let lower = roaster_name.to_lowercase();

    // Exact match first
    if let Some(roaster) = roasters.iter().find(|r| r.name.to_lowercase() == lower) {
        return Some(roaster.id.to_string());
    }

    // Substring match (either direction)
    roasters
        .iter()
        .find(|r| {
            let r_lower = r.name.to_lowercase();
            r_lower.contains(&lower) || lower.contains(&r_lower)
        })
        .map(|r| r.id.to_string())
}

define_list_fragment_renderer!(
    render_roast_list_fragment,
    RoastSortKey,
    load_roast_page,
    RoastListTemplate { roasts },
    "#roast-list"
);
