use argon2::{Argon2, password_hash::PasswordHasher};
use db::sqlx;
use model::users::UserTokenModel;
use rand::{RngExt, distr::Alphanumeric};
use serde::{Deserialize, Serialize};
use session::Session;

use crate::{AppService, error::AppError, session_user};

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserAccessToken {
    pub id: i64,
    pub name: String,
    pub scopes: Vec<String>,
    #[schema(value_type = Option<String>)]
    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
    pub is_revoked: bool,
    #[schema(value_type = String)]
    pub created_at: chrono::DateTime<chrono::Utc>,
    #[schema(value_type = String)]
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct CreatedUserAccessToken {
    pub token: String,
    pub access_token: UserAccessToken,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateUserAccessToken {
    pub name: String,
    pub scopes: Vec<String>,
    #[schema(value_type = Option<String>)]
    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateUserAccessToken {
    pub name: Option<String>,
    pub scopes: Option<Vec<String>>,
    #[schema(value_type = Option<String>)]
    pub expires_at: Option<Option<chrono::DateTime<chrono::Utc>>>,
}

impl AppService {
    pub async fn user_access_tokens(
        &self,
        ctx: &Session,
    ) -> Result<Vec<UserAccessToken>, AppError> {
        let user_uid = session_user(ctx)?;
        let rows = sqlx::query_as::<_, UserTokenModel>(
            "SELECT id, \"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at \
             FROM user_token WHERE \"user\" = $1 ORDER BY created_at DESC",
        )
        .bind(user_uid)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(rows.into_iter().map(Into::into).collect())
    }

    pub async fn user_create_access_token(
        &self,
        ctx: &Session,
        params: CreateUserAccessToken,
    ) -> Result<CreatedUserAccessToken, AppError> {
        let user_uid = session_user(ctx)?;
        let name = params.name.trim();
        if name.is_empty() {
            return Err(AppError::BadRequest(
                "token name is required".to_string(),
            ));
        }

        let token = generate_access_token();
        let token_hash = Argon2::default()
            .hash_password(token.as_bytes())
            .map_err(|e| AppError::PasswordHashError(e.to_string()))?
            .to_string();
        let scopes = normalize_scopes(params.scopes);
        let scopes_str = scopes.join(".");
        let now = chrono::Utc::now();

        let row = sqlx::query_as::<_, UserTokenModel>(
            "INSERT INTO user_token (\"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at) \
             VALUES ($1, $2, $3, $4, $5, false, $6, $6) \
             RETURNING id, \"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at",
        )
        .bind(user_uid)
        .bind(name)
        .bind(&token_hash)
        .bind(&scopes_str)
        .bind(params.expires_at)
        .bind(now)
        .fetch_one(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(CreatedUserAccessToken {
            token,
            access_token: row.into(),
        })
    }

    pub async fn user_revoke_access_token(
        &self,
        ctx: &Session,
        token_id: i64,
    ) -> Result<(), AppError> {
        let user_uid = session_user(ctx)?;
        let affected = sqlx::query(
            "UPDATE user_token SET is_revoked = true, updated_at = $1 \
             WHERE id = $2 AND \"user\" = $3 AND is_revoked = false",
        )
        .bind(chrono::Utc::now())
        .bind(token_id)
        .bind(user_uid)
        .execute(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .rows_affected();

        if affected == 0 {
            return Err(AppError::NotFound(
                "access token not found".to_string(),
            ));
        }
        Ok(())
    }

    pub async fn user_update_access_token(
        &self,
        ctx: &Session,
        token_id: i64,
        params: UpdateUserAccessToken,
    ) -> Result<UserAccessToken, AppError> {
        let user_uid = session_user(ctx)?;
        let mut token =
            self.user_access_token_by_id(user_uid, token_id).await?;

        if let Some(name) = params.name {
            let name = name.trim();
            if name.is_empty() {
                return Err(AppError::BadRequest(
                    "token name is required".to_string(),
                ));
            }
            token.name = name.to_string();
        }
        if let Some(scopes) = params.scopes {
            token.scopes = normalize_scopes(scopes);
        }
        if let Some(expires_at) = params.expires_at {
            token.expires_at = expires_at;
        }

        let row = sqlx::query_as::<_, UserTokenModel>(
            "UPDATE user_token SET name = $1, scopes = $2, expires_at = $3, updated_at = $4 \
             WHERE id = $5 AND \"user\" = $6 AND is_revoked = false \
             RETURNING id, \"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at",
        )
        .bind(&token.name)
        .bind(token.scopes.join("."))
        .bind(token.expires_at)
        .bind(chrono::Utc::now())
        .bind(token_id)
        .bind(user_uid)
        .fetch_optional(self.db.writer())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or_else(|| AppError::NotFound("access token not found".to_string()))?;

        Ok(row.into())
    }

    async fn user_access_token_by_id(
        &self,
        user_uid: uuid::Uuid,
        token_id: i64,
    ) -> Result<UserAccessToken, AppError> {
        sqlx::query_as::<_, UserTokenModel>(
            "SELECT id, \"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at \
             FROM user_token WHERE id = $1 AND \"user\" = $2 AND is_revoked = false",
        )
        .bind(token_id)
        .bind(user_uid)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .map(Into::into)
        .ok_or_else(|| AppError::NotFound("access token not found".to_string()))
    }
}

fn normalize_scopes(scopes: Vec<String>) -> Vec<String> {
    scopes
        .into_iter()
        .map(|scope| scope.trim().to_string())
        .filter(|scope| !scope.is_empty() && !scope.contains('.'))
        .collect()
}

fn generate_access_token() -> String {
    #[allow(deprecated)]
    let mut rng = rand::rng();
    let token: String =
        (0..64).map(|_| rng.sample(Alphanumeric) as char).collect();
    format!("gda_{}", token)
}

impl From<UserTokenModel> for UserAccessToken {
    fn from(value: UserTokenModel) -> Self {
        Self {
            id: value.id,
            name: value.name,
            scopes: value
                .scopes
                .split('.')
                .filter(|scope| !scope.is_empty())
                .map(ToString::to_string)
                .collect(),
            expires_at: value.expires_at,
            is_revoked: value.is_revoked,
            created_at: value.created_at,
            updated_at: value.updated_at,
        }
    }
}
