use axum::{
    Form, Json,
    extract::{Extension, Path, Query, State},
    http::StatusCode,
    response::{IntoResponse, Redirect},
};
use uuid::Uuid;

use application::diary::{
    commands::{DeleteReviewCommand, EditReviewCommand},
    delete_review, edit_review, get_activity_feed as get_feed_uc, get_diary, log_review,
    queries::GetActivityFeedQuery,
};

use crate::{
    csrf::CsrfToken,
    errors::ApiError,
    extractors::{AuthenticatedUser, OptionalCookieUser, RequiredCookieUser},
    forms::{ErrorQuery, FeedQueryParams, LogReviewData, LogReviewForm, to_diary_query},
    render::render_page,
    state::AppState,
};
use api_types::{
    ActivityFeedQueryParams, ActivityFeedResponse, DiaryQueryParams, DiaryResponse,
    EditReviewRequest, ExportQueryParams, LogReviewRequest,
};
use template_askama::{ActivityFeedTemplate, NewReviewTemplate, build_page_items};

use super::helpers::{build_export_response, build_page_context, encode_error};

// ── API ──────────────────────────────────────────────────────────────────────

#[utoipa::path(
    get, path = "/api/v1/diary",
    params(DiaryQueryParams),
    responses(
        (status = 200, body = DiaryResponse),
        (status = 401, description = "Unauthorized"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn get_diary(
    State(state): State<AppState>,
    Query(params): Query<DiaryQueryParams>,
) -> Result<Json<DiaryResponse>, ApiError> {
    let page =
        get_diary::execute(&state.app_ctx.deps.diary.get_diary, to_diary_query(params)).await?;

    Ok(Json(DiaryResponse {
        items: page
            .items
            .iter()
            .map(crate::mappers::movies::entry_to_dto)
            .collect(),
        total_count: page.total_count,
        limit: page.limit,
        offset: page.offset,
    }))
}

#[utoipa::path(
    post, path = "/api/v1/reviews",
    request_body = LogReviewRequest,
    responses(
        (status = 201, description = "Review created"),
        (status = 400, description = "Invalid input"),
        (status = 401, description = "Unauthorized"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn post_review(
    State(state): State<AppState>,
    user: AuthenticatedUser,
    Json(req): Json<LogReviewRequest>,
) -> Result<impl IntoResponse, ApiError> {
    let data = LogReviewData::try_from(req).map_err(ApiError)?;
    log_review::execute(
        &state.app_ctx.services.review_logger,
        data.into_command(user.0.value()),
    )
    .await?;
    Ok(StatusCode::CREATED)
}

#[utoipa::path(
    delete, path = "/api/v1/reviews/{id}",
    params(("id" = Uuid, Path, description = "Review ID")),
    responses(
        (status = 204, description = "Review deleted"),
        (status = 401, description = "Unauthorized"),
        (status = 403, description = "Forbidden"),
        (status = 404, description = "Review not found"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn delete_review(
    State(state): State<AppState>,
    AuthenticatedUser(user_id): AuthenticatedUser,
    Path(review_id): Path<Uuid>,
) -> Result<StatusCode, ApiError> {
    let cmd = DeleteReviewCommand {
        review_id,
        requesting_user_id: user_id.value(),
    };
    delete_review::execute(&state.app_ctx.deps.diary.delete_review, cmd).await?;
    Ok(StatusCode::NO_CONTENT)
}

#[utoipa::path(
    patch, path = "/api/v1/reviews/{id}",
    request_body = EditReviewRequest,
    params(("id" = Uuid, Path, description = "Review ID")),
    responses(
        (status = 200, description = "Review updated"),
        (status = 400, description = "Invalid input"),
        (status = 401, description = "Unauthorized"),
        (status = 403, description = "Forbidden"),
        (status = 404, description = "Review not found"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn patch_review(
    State(state): State<AppState>,
    AuthenticatedUser(user_id): AuthenticatedUser,
    Path(review_id): Path<Uuid>,
    Json(req): Json<EditReviewRequest>,
) -> Result<StatusCode, ApiError> {
    let watched_at = req
        .watched_at
        .map(|s| domain::value_objects::parse_watched_at(&s).map_err(ApiError))
        .transpose()?;

    let cmd = EditReviewCommand {
        review_id,
        requesting_user_id: user_id.value(),
        rating: req.rating,
        comment: req.comment,
        watched_at,
        watch_medium: req.watch_medium,
    };
    edit_review::execute(&state.app_ctx.deps.diary.edit_review, cmd).await?;
    Ok(StatusCode::OK)
}

#[utoipa::path(
    get, path = "/api/v1/diary/export",
    params(ExportQueryParams),
    responses(
        (status = 200, description = "Diary file download", content_type = "text/csv"),
        (status = 400, description = "Invalid format parameter"),
        (status = 401, description = "Unauthorized"),
    ),
    security(("bearer_auth" = []))
)]
pub async fn export_diary(
    State(state): State<AppState>,
    user: AuthenticatedUser,
    Query(params): Query<ExportQueryParams>,
) -> impl IntoResponse {
    build_export_response(&params.format, user.0.value(), &state)
}

#[utoipa::path(
    get, path = "/api/v1/activity-feed",
    params(ActivityFeedQueryParams),
    responses((status = 200, body = ActivityFeedResponse)),
)]
pub async fn get_activity_feed(
    State(state): State<AppState>,
    Query(params): Query<ActivityFeedQueryParams>,
) -> Result<Json<ActivityFeedResponse>, ApiError> {
    let page = get_feed_uc::execute(
        &state.app_ctx.deps.diary.get_activity_feed,
        GetActivityFeedQuery {
            limit: params.limit.unwrap_or(20),
            offset: params.offset.unwrap_or(0),
            sort_by: params
                .sort_by
                .as_deref()
                .map(|s| s.parse().unwrap_or_default())
                .unwrap_or_default(),
            search: None,
            viewer_user_id: None,
            filter_following: false,
        },
    )
    .await?;
    Ok(Json(ActivityFeedResponse {
        items: page
            .items
            .iter()
            .map(crate::mappers::diary::feed_entry_to_dto)
            .collect(),
        total_count: page.total_count,
        limit: page.limit,
        offset: page.offset,
    }))
}

// ── HTML ─────────────────────────────────────────────────────────────────────

pub async fn get_new_review_page(
    RequiredCookieUser(user_id): RequiredCookieUser,
    State(state): State<AppState>,
    Query(params): Query<ErrorQuery>,
    Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
    let mut ctx = build_page_context(&state, Some(user_id), csrf.0).await;
    ctx.page_title = "Log a Review — Movies Diary".to_string();
    ctx.canonical_url = format!("{}/reviews/new", state.app_ctx.config.base_url);
    render_page(NewReviewTemplate {
        ctx: &ctx,
        error: params.error.as_deref(),
    })
}

pub async fn post_review_html(
    State(state): State<AppState>,
    RequiredCookieUser(user_id): RequiredCookieUser,
    Extension(csrf): Extension<CsrfToken>,
    Form(form): Form<LogReviewForm>,
) -> impl IntoResponse {
    if crate::csrf::mismatch(&csrf, &form.csrf_token) {
        return StatusCode::FORBIDDEN.into_response();
    }
    let data = match LogReviewData::try_from(form) {
        Ok(d) => d,
        Err(_) => {
            return Redirect::to("/reviews/new?error=Invalid+date+format").into_response();
        }
    };

    match log_review::execute(
        &state.app_ctx.services.review_logger,
        data.into_command(user_id.value()),
    )
    .await
    {
        Ok(_) => Redirect::to("/").into_response(),
        Err(e) => {
            let msg = encode_error(&e.to_string());
            Redirect::to(&format!("/reviews/new?error={}", msg)).into_response()
        }
    }
}

pub async fn post_delete_review_html(
    State(state): State<AppState>,
    RequiredCookieUser(user_id): RequiredCookieUser,
    Extension(csrf): Extension<CsrfToken>,
    Path(review_id): Path<Uuid>,
    Form(form): Form<crate::forms::DeleteRedirectForm>,
) -> impl IntoResponse {
    if crate::csrf::mismatch(&csrf, &form.csrf_token) {
        return StatusCode::FORBIDDEN.into_response();
    }
    let cmd = DeleteReviewCommand {
        review_id,
        requesting_user_id: user_id.value(),
    };
    match delete_review::execute(&state.app_ctx.deps.diary.delete_review, cmd).await {
        Ok(()) => {
            let redirect_url = form
                .redirect_after
                .filter(|url| {
                    (url.starts_with('/') && !url.starts_with("//")) || url.starts_with('?')
                })
                .unwrap_or_else(|| "/".to_string());
            Redirect::to(&redirect_url).into_response()
        }
        Err(e) => crate::errors::domain_error_response(e),
    }
}

pub async fn get_export_html(
    State(state): State<AppState>,
    RequiredCookieUser(user_id): RequiredCookieUser,
    Query(params): Query<api_types::ExportQueryParams>,
) -> impl IntoResponse {
    build_export_response(&params.format, user_id.value(), &state)
}

pub async fn get_activity_feed_html(
    OptionalCookieUser(user_id): OptionalCookieUser,
    State(state): State<AppState>,
    Query(params): Query<FeedQueryParams>,
    Extension(csrf): Extension<CsrfToken>,
) -> impl IntoResponse {
    let ctx = build_page_context(&state, user_id.clone(), csrf.0).await;
    let limit = params.limit.unwrap_or(20);
    let offset = params.offset.unwrap_or(0);

    let filter_following = params.filter == "following" && user_id.is_some();
    let filter_str = if filter_following { "following" } else { "all" };

    let sort_by_str = match params.sort_by.as_str() {
        "date_asc" => "date_asc",
        "rating" => "rating",
        "rating_asc" => "rating_asc",
        _ => "date",
    };

    let query = application::diary::queries::GetActivityFeedQuery {
        limit,
        offset,
        sort_by: sort_by_str.parse().unwrap_or_default(),
        search: if params.search.is_empty() {
            None
        } else {
            Some(params.search.clone())
        },
        viewer_user_id: user_id.map(|u| u.value()),
        filter_following,
    };

    match application::diary::get_activity_feed::execute(
        &state.app_ctx.deps.diary.get_activity_feed,
        query,
    )
    .await
    {
        Ok(entries) => {
            let entry_limit = entries.limit;
            let entry_offset = entries.offset;
            let has_more =
                (entry_offset as u64).saturating_add(entry_limit as u64) < entries.total_count;
            let total_pages = (entries.total_count as u32)
                .saturating_add(entry_limit.saturating_sub(1))
                .checked_div(entry_limit)
                .unwrap_or(1);
            let current_page = entry_offset.checked_div(entry_limit).unwrap_or(0);
            let page_items = build_page_items(total_pages, current_page);
            render_page(ActivityFeedTemplate {
                entries: entries.items.as_slice(),
                current_offset: entry_offset,
                limit: entry_limit,
                has_more,
                ctx: &ctx,
                page_items,
                filter: filter_str.to_string(),
                sort_by: sort_by_str.to_string(),
                search: params.search,
            })
            .into_response()
        }
        Err(e) => crate::errors::domain_error_response(e),
    }
}
