use db::sqlx;
use model::workspace::{WkMemberModel, WorkspaceModel};
use serde::{Deserialize, Serialize};
use session::Session;
use storage::{ObjectStorage, PutObjectOptions};

use super::types::{
    WorkspaceListRow, WorkspaceResponse, normalize_name, workspace_response,
};
use crate::{AppService, error::AppError, session_user};

const ALLOWED_AVATAR_TYPES: &[&str] =
    &["image/png", "image/jpeg", "image/webp", "image/gif"];
const MAX_AVATAR_SIZE: usize = 5 * 1024 * 1024;

#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
pub struct AvatarUploadResponse {
    pub avatar_url: String,
}

fn extension_from_content_type(content_type: &str) -> &str {
    match content_type {
        "image/png" => "png",
        "image/jpeg" => "jpg",
        "image/webp" => "webp",
        "image/gif" => "gif",
        _ => "bin",
    }
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct CreateWorkspace {
    pub name: String,
    pub description: Option<String>,
    pub avatar_url: Option<String>,
}

#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateWorkspace {
    pub name: Option<String>,
    pub description: Option<String>,
    pub avatar_url: Option<String>,
}

impl AppService {
    pub async fn workspace_create(
        &self,
        ctx: &Session,
        params: CreateWorkspace,
    ) -> Result<WorkspaceResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let name = normalize_name(&params.name)?;
        self.workspace_ensure_name_available(&name).await?;

        let wk_id = uuid::Uuid::now_v7();
        let now = chrono::Utc::now();
        let description = params.description.unwrap_or_default();
        let avatar_url = params.avatar_url.unwrap_or_default();
        let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;

        let workspace = sqlx::query_as::<_, WorkspaceModel>(
            "INSERT INTO workspace (id, name, description, avatar_url, created_at) \
             VALUES ($1, $2, $3, $4, $5) \
             RETURNING id, name, description, avatar_url, created_at",
        )
        .bind(wk_id)
        .bind(&name)
        .bind(&description)
        .bind(&avatar_url)
        .bind(now)
        .fetch_one(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        sqlx::query(
            "INSERT INTO wk_member (wk, \"user\", owner, admin, join_at, leave_at) \
             VALUES ($1, $2, true, true, $3, NULL)",
        )
        .bind(wk_id)
        .bind(user_uid)
        .bind(now)
        .execute(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        txn.commit().await.map_err(|_| AppError::TxnError)?;
        Ok(workspace_response(workspace, true, true))
    }

    pub async fn workspace_my(
        &self,
        ctx: &Session,
    ) -> Result<Vec<WorkspaceResponse>, AppError> {
        let user_uid = session_user(ctx)?;

        let rows = sqlx::query_as::<_, WorkspaceListRow>(
            "SELECT w.id, w.name, w.description, w.avatar_url, w.created_at, m.owner, m.admin \
             FROM wk_member m \
             INNER JOIN workspace w ON w.id = m.wk \
             WHERE m.\"user\" = $1 AND m.leave_at IS NULL \
             ORDER BY w.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(|row| {
                let owner = row.owner;
                let admin = row.admin;
                workspace_response(row.into(), owner, admin)
            })
            .collect())
    }

    pub async fn workspace_get(
        &self,
        ctx: &Session,
        name: &str,
    ) -> Result<WorkspaceResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        let member = self.workspace_member(wk.id, user_uid).await?;
        Ok(workspace_response(wk, member.owner, member.admin))
    }

    pub async fn workspace_update(
        &self,
        ctx: &Session,
        name: &str,
        params: UpdateWorkspace,
    ) -> Result<WorkspaceResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let mut wk = self.workspace_resolve(name).await?;
        self.workspace_require_admin(wk.id, user_uid).await?;

        let next_name = match params.name {
            Some(name) => {
                let name = normalize_name(&name)?;
                if name != wk.name {
                    self.workspace_ensure_name_available(&name).await?;
                    Some(name)
                } else {
                    None
                }
            }
            None => None,
        };
        let next_avatar_url =
            params.avatar_url.unwrap_or_else(|| wk.avatar_url.clone());
        let next_description =
            params.description.unwrap_or_else(|| wk.description.clone());

        let mut txn = self.db.begin().await.map_err(|_| AppError::TxnError)?;
        if let Some(next_name) = &next_name {
            sqlx::query(
                "INSERT INTO wk_history_name (id, wk, name, changed_by, created_at) \
                 VALUES ($1, $2, $3, $4, $5)",
            )
            .bind(uuid::Uuid::now_v7())
            .bind(wk.id)
            .bind(&wk.name)
            .bind(user_uid)
            .bind(chrono::Utc::now())
            .execute(&mut **txn.inner_mut())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;
            wk.name = next_name.clone();
        }

        wk = sqlx::query_as::<_, WorkspaceModel>(
            "UPDATE workspace SET name = $1, description = $2, avatar_url = $3 WHERE id = $4 \
             RETURNING id, name, description, avatar_url, created_at",
        )
        .bind(&wk.name)
        .bind(&next_description)
        .bind(&next_avatar_url)
        .bind(wk.id)
        .fetch_one(&mut **txn.inner_mut())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        txn.commit().await.map_err(|_| AppError::TxnError)?;

        let member = self.workspace_member(wk.id, user_uid).await?;
        Ok(workspace_response(wk, member.owner, member.admin))
    }

    /// Get a workspace's avatar URL by workspace name.
    pub async fn workspace_get_avatar_url(
        &self,
        name: &str,
    ) -> Result<String, AppError> {
        let wk = self.workspace_resolve(name).await?;
        if wk.avatar_url.is_empty() {
            return Err(AppError::NotFound("avatar not found".to_string()));
        }
        Ok(wk.avatar_url)
    }

    /// Upload a workspace avatar image, store it, and update the workspace's avatar_url.
    pub async fn workspace_upload_avatar(
        &self,
        ctx: &Session,
        name: &str,
        bytes: Vec<u8>,
        content_type: &str,
    ) -> Result<AvatarUploadResponse, AppError> {
        let user_uid = session_user(ctx)?;
        let wk = self.workspace_resolve(name).await?;
        self.workspace_require_admin(wk.id, user_uid).await?;

        if bytes.len() > MAX_AVATAR_SIZE {
            return Err(AppError::AvatarUploadError(
                "file size exceeds 5 MB limit".to_string(),
            ));
        }
        if !ALLOWED_AVATAR_TYPES.contains(&content_type) {
            return Err(AppError::AvatarUploadError(format!(
                "unsupported image type: {content_type}. Allowed: png, jpeg, webp, gif"
            )));
        }

        let ext = extension_from_content_type(content_type);
        let key = format!(
            "avatars/workspaces/{wk_id}-{ts}.{ext}",
            wk_id = wk.id,
            ts = uuid::Uuid::now_v7()
        );

        let stored = self
            .storage
            .put_bytes(
                &key,
                bytes,
                PutObjectOptions {
                    content_type: Some(content_type.to_string()),
                    ..PutObjectOptions::default()
                },
            )
            .await
            .map_err(|e| {
                AppError::AvatarUploadError(format!("storage error: {e}"))
            })?;

        sqlx::query("UPDATE workspace SET avatar_url = $1 WHERE id = $2")
            .bind(&stored.url)
            .bind(wk.id)
            .execute(self.db.writer())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        Ok(AvatarUploadResponse {
            avatar_url: stored.url,
        })
    }

    pub async fn workspace_resolve(
        &self,
        name: &str,
    ) -> Result<WorkspaceModel, AppError> {
        if let Some(wk) = sqlx::query_as::<_, WorkspaceModel>(
            "SELECT id, name, description, avatar_url, created_at FROM workspace WHERE name = $1",
        )
        .bind(name)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        {
            return Ok(wk);
        }

        if let Some(history) = sqlx::query_as::<_, (uuid::Uuid,)>(
            "SELECT wk FROM wk_history_name WHERE name = $1 ORDER BY created_at DESC LIMIT 1",
        )
        .bind(name)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        {
            return sqlx::query_as::<_, WorkspaceModel>(
                "SELECT id, name, description, avatar_url, created_at FROM workspace WHERE id = $1",
            )
            .bind(history.0)
            .fetch_optional(self.db.reader())
            .await
            .map_err(|e| AppError::DatabaseError(e.to_string()))?
            .ok_or(AppError::NotFound("workspace not found".to_string()));
        }

        Err(AppError::NotFound("workspace not found".to_string()))
    }

    pub async fn workspace_member(
        &self,
        wk_id: uuid::Uuid,
        user_uid: uuid::Uuid,
    ) -> Result<WkMemberModel, AppError> {
        sqlx::query_as::<_, WkMemberModel>(
            "SELECT wk, \"user\", owner, admin, join_at, leave_at \
             FROM wk_member WHERE wk = $1 AND \"user\" = $2 AND leave_at IS NULL",
        )
        .bind(wk_id)
        .bind(user_uid)
        .fetch_optional(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?
        .ok_or(AppError::PermissionDenied)
    }

    pub async fn workspace_require_member(
        &self,
        wk_id: uuid::Uuid,
        user_uid: uuid::Uuid,
    ) -> Result<WkMemberModel, AppError> {
        self.workspace_member(wk_id, user_uid).await
    }

    pub async fn workspace_require_admin(
        &self,
        wk_id: uuid::Uuid,
        user_uid: uuid::Uuid,
    ) -> Result<WkMemberModel, AppError> {
        let member = self.workspace_member(wk_id, user_uid).await?;
        if member.owner || member.admin {
            Ok(member)
        } else {
            Err(AppError::PermissionDenied)
        }
    }

    pub async fn workspace_require_owner(
        &self,
        wk_id: uuid::Uuid,
        user_uid: uuid::Uuid,
    ) -> Result<WkMemberModel, AppError> {
        let member = self.workspace_member(wk_id, user_uid).await?;
        if member.owner {
            Ok(member)
        } else {
            Err(AppError::PermissionDenied)
        }
    }

    async fn workspace_ensure_name_available(
        &self,
        name: &str,
    ) -> Result<(), AppError> {
        let current = sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM workspace WHERE name = $1)",
        )
        .bind(name)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        let history = sqlx::query_scalar::<_, bool>(
            "SELECT EXISTS(SELECT 1 FROM wk_history_name WHERE name = $1)",
        )
        .bind(name)
        .fetch_one(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;

        if current || history {
            Err(AppError::Conflict(
                "workspace name already exists".to_string(),
            ))
        } else {
            Ok(())
        }
    }

    pub async fn workspace_my_inner(
        &self,
        user_id: uuid::Uuid,
    ) -> Result<Vec<(String, Option<String>)>, AppError> {
        let rows = sqlx::query_as::<_, (String, Option<String>)>(
            "SELECT w.name, w.description \
             FROM wk_member m \
             INNER JOIN workspace w ON w.id = m.wk \
             WHERE m.\"user\" = $1 AND m.leave_at IS NULL \
             ORDER BY w.created_at DESC",
        )
        .bind(user_id)
        .fetch_all(self.db.reader())
        .await
        .map_err(|e| AppError::DatabaseError(e.to_string()))?;
        Ok(rows)
    }
}
