use axum::{
    extract::{Form, Path, State},
    http::StatusCode,
    response::{IntoResponse, Redirect, Response},
};
use askama::Template;
use askama_web::WebTemplate;
use serde::Deserialize;
use sqlx::FromRow;

use crate::auth_guard::{AuthUser, OptionalAuth};
use crate::state::AppState;

const MAX_PASTE_SIZE: usize = 512 * 1024;
const FREE_PASTE_LIMIT: i64 = 5;
const FREE_EXPIRY_HOURS: i64 = 24;

#[derive(FromRow)]
#[allow(dead_code)]
struct Paste {
    id: String,
    user_id: String,
    title: Option<String>,
    content: String,
    language: Option<String>,
    size_bytes: i64,
    created_at: time::OffsetDateTime,
    expires_at: Option<time::OffsetDateTime>,
}

fn format_time(t: time::OffsetDateTime) -> String {
    format!(
        "{:04}-{:02}-{:02} {:02}:{:02} UTC",
        t.year(),
        t.month() as u8,
        t.day(),
        t.hour(),
        t.minute(),
    )
}

#[derive(Template, WebTemplate)]
#[template(path = "create.html")]
#[allow(dead_code)]
pub struct CreateTemplate {
    pub logged_in: bool,
    pub email: Option<String>,
}

pub async fn index(OptionalAuth(user): OptionalAuth) -> CreateTemplate {
    CreateTemplate {
        logged_in: user.is_some(),
        email: user.and_then(|u| u.email),
    }
}

#[derive(Deserialize)]
pub struct CreateForm {
    title: Option<String>,
    content: String,
    language: Option<String>,
}

pub async fn create(
    State(state): State<AppState>,
    AuthUser(user): AuthUser,
    Form(form): Form<CreateForm>,
) -> Result<Redirect, Response> {
    if form.content.is_empty() {
        return Err((StatusCode::BAD_REQUEST, "paste content cannot be empty").into_response());
    }

    if form.content.len() > MAX_PASTE_SIZE {
        return Err((StatusCode::BAD_REQUEST, "paste too large (512KB max)").into_response());
    }

    let is_pro = user.is_pro();

    if !is_pro {
        let count: (i64,) = sqlx::query_as("SELECT count(*) FROM pastes WHERE user_id = $1")
            .bind(&user.sub)
            .fetch_one(&state.db)
            .await
            .map_err(|e| {
                tracing::error!("paste count query failed: {e}");
                (StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response()
            })?;

        if count.0 >= FREE_PASTE_LIMIT {
            return Err((
                StatusCode::FORBIDDEN,
                "free plan limited to 5 pastes -- upgrade to pro for unlimited",
            )
                .into_response());
        }
    }

    let id = nanoid::nanoid!(8);
    let size_bytes = form.content.len() as i64;
    let title = form.title.filter(|t| !t.trim().is_empty());
    let language = form.language.filter(|l| !l.trim().is_empty());

    let expires_at = if is_pro {
        None
    } else {
        Some(time::OffsetDateTime::now_utc() + time::Duration::hours(FREE_EXPIRY_HOURS))
    };

    sqlx::query(
        "INSERT INTO pastes (id, user_id, title, content, language, size_bytes, expires_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7)",
    )
    .bind(&id)
    .bind(&user.sub)
    .bind(&title)
    .bind(&form.content)
    .bind(&language)
    .bind(size_bytes)
    .bind(expires_at)
    .execute(&state.db)
    .await
    .map_err(|e| {
        tracing::error!("paste insert failed: {e}");
        (StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response()
    })?;

    Ok(Redirect::see_other(&format!("/{id}")))
}

#[derive(Template, WebTemplate)]
#[template(path = "view.html")]
pub struct ViewTemplate {
    pub id: String,
    pub title: String,
    pub content: String,
    pub language: String,
    pub created_at: String,
    pub expires_at: Option<String>,
    pub is_owner: bool,
}

pub async fn view(
    State(state): State<AppState>,
    OptionalAuth(user): OptionalAuth,
    Path(id): Path<String>,
) -> Result<ViewTemplate, StatusCode> {
    let paste = sqlx::query_as::<_, Paste>("SELECT * FROM pastes WHERE id = $1")
        .bind(&id)
        .fetch_optional(&state.db)
        .await
        .map_err(|e| {
            tracing::error!("paste query failed: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    let is_owner = user.map(|u| u.sub == paste.user_id).unwrap_or(false);

    Ok(ViewTemplate {
        id: paste.id,
        title: paste.title.unwrap_or_else(|| "untitled".to_string()),
        content: paste.content,
        language: paste.language.unwrap_or_else(|| "plaintext".to_string()),
        created_at: format_time(paste.created_at),
        expires_at: paste.expires_at.map(format_time),
        is_owner,
    })
}

pub async fn raw(
    State(state): State<AppState>,
    Path(id): Path<String>,
) -> Result<Response, StatusCode> {
    let paste = sqlx::query_as::<_, Paste>("SELECT * FROM pastes WHERE id = $1")
        .bind(&id)
        .fetch_optional(&state.db)
        .await
        .map_err(|e| {
            tracing::error!("paste query failed: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?
        .ok_or(StatusCode::NOT_FOUND)?;

    Ok((
        [(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")],
        paste.content,
    )
        .into_response())
}

struct PasteListRow {
    id: String,
    title: Option<String>,
    language: Option<String>,
    size_bytes: i64,
    created_at: time::OffsetDateTime,
    expires_at: Option<time::OffsetDateTime>,
}

impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for PasteListRow {
    fn from_row(row: &sqlx::postgres::PgRow) -> Result<Self, sqlx::Error> {
        use sqlx::Row;
        Ok(Self {
            id: row.try_get("id")?,
            title: row.try_get("title")?,
            language: row.try_get("language")?,
            size_bytes: row.try_get("size_bytes")?,
            created_at: row.try_get("created_at")?,
            expires_at: row.try_get("expires_at")?,
        })
    }
}

pub struct PasteEntry {
    pub id: String,
    pub title: String,
    pub language: String,
    pub size: String,
    pub created_at: String,
    pub expires_at: Option<String>,
}

#[derive(Template, WebTemplate)]
#[template(path = "my.html")]
pub struct MyTemplate {
    pub pastes: Vec<PasteEntry>,
}

fn format_size(bytes: i64) -> String {
    if bytes < 1024 {
        format!("{bytes} B")
    } else {
        format!("{:.1} KB", bytes as f64 / 1024.0)
    }
}

pub async fn my_pastes(
    State(state): State<AppState>,
    AuthUser(user): AuthUser,
) -> Result<MyTemplate, StatusCode> {
    let rows = sqlx::query_as::<_, PasteListRow>(
        "SELECT id, title, language, size_bytes, created_at, expires_at
         FROM pastes WHERE user_id = $1 ORDER BY created_at DESC",
    )
    .bind(&user.sub)
    .fetch_all(&state.db)
    .await
    .map_err(|e| {
        tracing::error!("paste list query failed: {e}");
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    let pastes = rows
        .into_iter()
        .map(|r| PasteEntry {
            id: r.id,
            title: r.title.unwrap_or_else(|| "untitled".to_string()),
            language: r.language.unwrap_or_else(|| "plaintext".to_string()),
            size: format_size(r.size_bytes),
            created_at: format_time(r.created_at),
            expires_at: r.expires_at.map(format_time),
        })
        .collect();

    Ok(MyTemplate { pastes })
}

pub async fn delete(
    State(state): State<AppState>,
    AuthUser(user): AuthUser,
    Path(id): Path<String>,
) -> Result<Redirect, StatusCode> {
    let result = sqlx::query("DELETE FROM pastes WHERE id = $1 AND user_id = $2")
        .bind(&id)
        .bind(&user.sub)
        .execute(&state.db)
        .await
        .map_err(|e| {
            tracing::error!("paste delete failed: {e}");
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

    if result.rows_affected() == 0 {
        return Err(StatusCode::NOT_FOUND);
    }

    Ok(Redirect::see_other("/my"))
}
