use std::path::PathBuf;

use actix_web::{Error, HttpRequest, HttpResponse, web};
use argon2::{
    Argon2,
    password_hash::{PasswordHash, PasswordVerifier},
};
use model::{
    repos::RepoModel,
    users::{user::UserModel, user_token::UserTokenModel},
};

use crate::{
    errors::GitError,
    http::{
        HttpAppState,
        auth::{authorize_repo_access, verify_access_token},
        handler::is_valid_lfs_oid,
        lfs::{BatchRequest, CreateLockRequest, LfsHandler},
        utils::{extract_basic_credentials, get_repo_model},
    },
    ssh::authz::SshAuthService,
    sync::push_queue::{
        PushQueueEvent, PushQueueLease, PushQueueWaitError,
        wait_for_push_queue_slot,
    },
};

fn base_url(req: &HttpRequest) -> String {
    let conn_info = req.connection_info();
    format!("{}://{}", conn_info.scheme(), conn_info.host())
}

fn bearer_token(req: &HttpRequest) -> Result<String, Error> {
    let auth_header = req
        .headers()
        .get("authorization")
        .ok_or_else(|| {
            actix_web::error::ErrorUnauthorized("Missing authorization header")
        })?
        .to_str()
        .map_err(|_| {
            actix_web::error::ErrorUnauthorized("Invalid authorization header")
        })?;

    if let Some(token) = auth_header.strip_prefix("Bearer ") {
        Ok(token.to_string())
    } else {
        Err(actix_web::error::ErrorUnauthorized(
            "Invalid authorization format",
        ))
    }
}

async fn user_uid(
    req: &HttpRequest,
    db: &db::database::AppDatabase,
) -> Result<uuid::Uuid, Error> {
    if let Ok((username, access_key)) = extract_basic_credentials(req) {
        return verify_access_token(db, &username, &access_key)
            .await
            .map(|user| user.id);
    }

    let token = bearer_token(req)?;
    find_user_by_bearer_token(&token, db).await
}
pub async fn store_lfs_token(
    cache: &cache::AppCache,
    token: &str,
    repo_id: uuid::Uuid,
    user_uid: uuid::Uuid,
    operation: &str,
) {
    if let Some(mut conn) = cache.conn() {
        use redis::AsyncCommands;
        let value = format!("{}:{}:{}", repo_id, user_uid, operation);
        let _: () = conn
            .set_ex(format!("lfs:token:{}", token), value, 3600_u64)
            .await
            .map_err(
                |e| tracing::warn!(error = %e, "failed to store lfs token"),
            )
            .unwrap_or(());
    }
}

async fn validate_lfs_token(
    token: &str,
    cache: &cache::AppCache,
    db: &db::database::AppDatabase,
    expected_repo_id: uuid::Uuid,
    expected_operation: &str,
) -> Result<uuid::Uuid, Error> {
    if let Some(mut conn) = cache.conn() {
        use redis::AsyncCommands;
        let stored: Option<String> = conn
            .get::<String, String>(format!("lfs:token:{}", token))
            .await
            .ok();
        if let Some(value) = stored {
            let parts: Vec<&str> = value.split(':').collect();
            if parts.len() == 3 {
                let repo_id =
                    uuid::Uuid::parse_str(parts[0]).map_err(|_| {
                        actix_web::error::ErrorUnauthorized(
                            "Invalid batch token",
                        )
                    })?;
                let user_uid =
                    uuid::Uuid::parse_str(parts[1]).map_err(|_| {
                        actix_web::error::ErrorUnauthorized(
                            "Invalid batch token",
                        )
                    })?;
                let operation = parts[2];

                if repo_id != expected_repo_id {
                    return Err(actix_web::error::ErrorUnauthorized(
                        "Token not valid for this repo",
                    ));
                }
                if operation != expected_operation {
                    return Err(actix_web::error::ErrorUnauthorized(
                        "Token not valid for this operation",
                    ));
                }

                let _: Result<(), redis::RedisError> =
                    conn.del(format!("lfs:token:{}", token)).await;

                return Ok(user_uid);
            }
        }
    }

    find_user_by_bearer_token(token, db).await
}

async fn find_user_by_bearer_token(
    token: &str,
    db: &db::database::AppDatabase,
) -> Result<uuid::Uuid, Error> {
    let tokens: Vec<UserTokenModel> = sqlx::query_as::<_, UserTokenModel>(
        "SELECT id, \"user\", name, token_hash, scopes, expires_at, is_revoked, created_at, updated_at \
         FROM user_token \
         WHERE is_revoked = false",
    )
    .fetch_all(db.reader())
    .await
    .map_err(|_| actix_web::error::ErrorUnauthorized("Authentication failed"))?;

    for token_model in tokens {
        if token_model
            .expires_at
            .map(|expires_at| expires_at < chrono::Utc::now())
            .unwrap_or(false)
        {
            continue;
        }

        let Ok(hash) = PasswordHash::new(&token_model.token_hash) else {
            tracing::warn!(
                token_id = token_model.id,
                "invalid stored bearer token hash"
            );
            continue;
        };
        if Argon2::default()
            .verify_password(token.as_bytes(), &hash)
            .is_ok()
        {
            return Ok(token_model.user);
        }
    }

    Err(actix_web::error::ErrorUnauthorized("Invalid token"))
}

async fn authorize_user_repo_access(
    db: &db::database::AppDatabase,
    user_uid: uuid::Uuid,
    repo: &RepoModel,
    is_write: bool,
) -> Result<(), Error> {
    let user = sqlx::query_as::<_, UserModel>(
        "SELECT id, username, display_name, avatar_url, website_url, allow_use, can_search, \
                last_sign_in_at, created_at, updated_at \
         FROM \"user\" \
         WHERE id = $1",
    )
    .bind(user_uid)
    .fetch_optional(db.reader())
    .await
    .map_err(|_| actix_web::error::ErrorUnauthorized("Authentication failed"))?
    .ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid token user"))?;

    let authz = SshAuthService::new(db.clone());
    if authz.check_repo_permission(&user, repo, is_write).await {
        Ok(())
    } else {
        Err(actix_web::error::ErrorForbidden(
            "No permission for repository",
        ))
    }
}

async fn acquire_lfs_write_queue(
    state: &HttpAppState,
    repo: &RepoModel,
    operation: &'static str,
) -> Result<PushQueueLease, Error> {
    match wait_for_push_queue_slot(
        state.sync.clone(),
        repo.id,
        |event, request_id| {
            let request_id = request_id.to_string();
            match event {
                PushQueueEvent::Waiting(position) => {
                    tracing::info!(
                        repo = %repo.name,
                        repo_id = %repo.id,
                        request_id = %request_id,
                        operation = operation,
                        position = position.position,
                        total = position.total,
                        "lfs_write_queue_waiting"
                    );
                }
                PushQueueEvent::Acquired => {
                    tracing::info!(
                        repo = %repo.name,
                        repo_id = %repo.id,
                        request_id = %request_id,
                        operation = operation,
                        "lfs_write_queue_acquired"
                    );
                }
            }
        },
    )
    .await
    {
        Ok(lease) => Ok(lease),
        Err(PushQueueWaitError::Join(e)) => {
            tracing::error!(
                error = %e,
                repo = %repo.name,
                repo_id = %repo.id,
                operation = operation,
                "lfs_write_queue_join_failed"
            );
            Err(actix_web::error::ErrorServiceUnavailable(
                "LFS write queue is temporarily unavailable. Please retry later.",
            ))
        }
        Err(PushQueueWaitError::Lock(e)) => {
            tracing::error!(
                error = %e,
                repo = %repo.name,
                repo_id = %repo.id,
                operation = operation,
                "lfs_write_queue_lock_failed"
            );
            Err(actix_web::error::ErrorServiceUnavailable(
                "LFS write queue lock failed. Please retry later.",
            ))
        }
        Err(PushQueueWaitError::Timeout) => {
            tracing::warn!(
                repo = %repo.name,
                repo_id = %repo.id,
                operation = operation,
                "lfs_write_queue_timeout"
            );
            Err(actix_web::error::ErrorServiceUnavailable(
                "LFS write queue timed out. Please retry in a moment.",
            ))
        }
    }
}

pub async fn lfs_batch(
    req: HttpRequest,
    path: web::Path<(String, String)>,
    body: web::Json<BatchRequest>,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name) = path.into_inner();
    let batch_req = body.into_inner();
    let is_write = batch_req.operation == "upload";

    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;

    if repo.visibility != "public" || is_write {
        let uid = user_uid(&req, &state.db).await?;
        authorize_repo_access(&req, &state.db, &repo, is_write).await?;

        let handler = LfsHandler::new(
            PathBuf::from(&repo.storage_path),
            repo,
            namespace,
            state.db.clone(),
        );
        let response = handler
            .batch_with_auth(batch_req, &base_url(&req), uid, &state.cache)
            .await
            .map_err(|_| {
                actix_web::error::ErrorInternalServerError("LFS batch failed")
            })?;
        Ok(HttpResponse::Ok()
            .content_type("application/vnd.git-lfs+json")
            .json(response))
    } else {
        let handler = LfsHandler::new(
            PathBuf::from(&repo.storage_path),
            repo,
            namespace,
            state.db.clone(),
        );
        let response = handler
            .batch(batch_req, &base_url(&req))
            .await
            .map_err(|_| {
                actix_web::error::ErrorInternalServerError("LFS batch failed")
            })?;
        Ok(HttpResponse::Ok()
            .content_type("application/vnd.git-lfs+json")
            .json(response))
    }
}

pub async fn lfs_upload(
    req: HttpRequest,
    path: web::Path<(String, String, String)>,
    payload: web::Payload,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name, oid) = path.into_inner();

    if !is_valid_lfs_oid(&oid) {
        return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
    }

    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
    let token = bearer_token(&req)?;

    let uid =
        validate_lfs_token(&token, &state.cache, &state.db, repo.id, "upload")
            .await?;
    authorize_user_repo_access(&state.db, uid, &repo, true).await?;

    let handler = LfsHandler::new(
        PathBuf::from(&repo.storage_path),
        repo.clone(),
        namespace,
        state.db.clone(),
    );
    let mut queue_lease =
        acquire_lfs_write_queue(&state, &handler.model, "upload").await?;

    let result = match handler.upload_object(&oid, payload).await {
        Ok(response) => Ok(response),
        Err(GitError::InvalidOid(_)) => {
            Err(actix_web::error::ErrorBadRequest("Invalid OID"))
        }
        Err(GitError::AuthFailed(_)) => {
            Err(actix_web::error::ErrorUnauthorized("Unauthorized"))
        }
        Err(_e) => {
            Err(actix_web::error::ErrorInternalServerError("Upload failed"))
        }
    };
    queue_lease.release().await;
    result
}

pub async fn lfs_download(
    req: HttpRequest,
    path: web::Path<(String, String, String)>,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name, oid) = path.into_inner();

    if !is_valid_lfs_oid(&oid) {
        return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
    }

    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;

    if repo.visibility != "public" {
        let token = bearer_token(&req)?;
        let uid = validate_lfs_token(
            &token,
            &state.cache,
            &state.db,
            repo.id,
            "download",
        )
        .await?;
        authorize_user_repo_access(&state.db, uid, &repo, false).await?;
    }

    let handler = LfsHandler::new(
        PathBuf::from(&repo.storage_path),
        repo,
        namespace,
        state.db.clone(),
    );

    match handler.download_object(&oid).await {
        Ok(response) => Ok(response),
        Err(GitError::NotFound(_)) => {
            Err(actix_web::error::ErrorNotFound("Object not found"))
        }
        Err(GitError::AuthFailed(_)) => {
            Err(actix_web::error::ErrorUnauthorized("Unauthorized"))
        }
        Err(_e) => Err(actix_web::error::ErrorInternalServerError(
            "Download failed",
        )),
    }
}

pub async fn lfs_lock_create(
    req: HttpRequest,
    path: web::Path<(String, String)>,
    body: web::Json<CreateLockRequest>,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name) = path.into_inner();

    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
    let uid = user_uid(&req, &state.db).await?;
    authorize_repo_access(&req, &state.db, &repo, true).await?;
    let handler = LfsHandler::new(
        PathBuf::from(&repo.storage_path),
        repo.clone(),
        namespace,
        state.db.clone(),
    );
    let mut queue_lease =
        acquire_lfs_write_queue(&state, &handler.model, "lock_create").await?;

    let result = match handler.lock_object(&body.oid, uid).await {
        Ok(lock) => Ok(HttpResponse::Created().json(lock)),
        Err(GitError::Locked(msg)) => Ok(HttpResponse::Conflict().body(msg)),
        Err(_e) => {
            Err(actix_web::error::ErrorInternalServerError("Lock failed"))
        }
    };
    queue_lease.release().await;
    result
}

pub async fn lfs_lock_list(
    req: HttpRequest,
    path: web::Path<(String, String)>,
    query: web::Query<std::collections::HashMap<String, String>>,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name) = path.into_inner();
    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;

    if repo.visibility != "public" {
        let uid = user_uid(&req, &state.db).await?;
        authorize_user_repo_access(&state.db, uid, &repo, false).await?;
    }

    let maybe_oid = query.get("oid").map(|s| s.as_str());
    let handler = LfsHandler::new(
        PathBuf::from(&repo.storage_path),
        repo,
        namespace,
        state.db.clone(),
    );

    match handler.list_locks(maybe_oid).await {
        Ok(list) => Ok(HttpResponse::Ok().json(list)),
        Err(_e) => Err(actix_web::error::ErrorInternalServerError(
            "Lock list failed",
        )),
    }
}

pub async fn lfs_lock_get(
    req: HttpRequest,
    path: web::Path<(String, String, String)>,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name, lock_id) = path.into_inner();
    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;

    if repo.visibility != "public" {
        let uid = user_uid(&req, &state.db).await?;
        authorize_user_repo_access(&state.db, uid, &repo, false).await?;
    }

    let handler = LfsHandler::new(
        PathBuf::from(&repo.storage_path),
        repo,
        namespace,
        state.db.clone(),
    );

    match handler.get_lock(&lock_id).await {
        Ok(lock) => Ok(HttpResponse::Ok().json(lock)),
        Err(GitError::NotFound(_)) => {
            Err(actix_web::error::ErrorNotFound("Lock not found"))
        }
        Err(_e) => Err(actix_web::error::ErrorInternalServerError(
            "Lock get failed",
        )),
    }
}

pub async fn lfs_lock_delete(
    req: HttpRequest,
    path: web::Path<(String, String, String)>,
    state: web::Data<HttpAppState>,
) -> Result<HttpResponse, Error> {
    let (namespace, repo_name, lock_id) = path.into_inner();

    let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
    let uid = user_uid(&req, &state.db).await?;
    authorize_repo_access(&req, &state.db, &repo, true).await?;
    let handler = LfsHandler::new(
        PathBuf::from(&repo.storage_path),
        repo.clone(),
        namespace,
        state.db.clone(),
    );
    let mut queue_lease =
        acquire_lfs_write_queue(&state, &handler.model, "lock_delete").await?;

    let result = match handler.unlock_object(&lock_id, uid).await {
        Ok(()) => Ok(HttpResponse::NoContent().finish()),
        Err(GitError::PermissionDenied(_)) => {
            Err(actix_web::error::ErrorForbidden("Not allowed"))
        }
        Err(GitError::NotFound(_)) => {
            Err(actix_web::error::ErrorNotFound("Lock not found"))
        }
        Err(_e) => Err(actix_web::error::ErrorInternalServerError(
            "Lock delete failed",
        )),
    };
    queue_lease.release().await;
    result
}
