use std::collections::HashMap;
use std::path::Path as FsPath;

use diesel::prelude::*;
use serde::Serialize;
use serde_json::Value;
use utoipa::ToSchema;
use uuid::Uuid;

use crate::error::{AppError, AppResult};
use crate::models::{Document, DocumentAsset, DocumentAssetObject, DocumentVersion};
use crate::schema::{document_asset_objects, document_assets, document_versions};
use crate::state::AppState;
use crate::utils::time::to_iso;

#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentAssetResponse {
    pub id: Uuid,
    pub asset_type: String,
    pub mime_type: String,
    #[schema(value_type = Object)]
    pub metadata: Value,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(nullable)]
    pub cardinality: Option<i32>,
}

#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentAssetObjectResponse {
    pub id: Uuid,
    pub ordinal: i32,
    #[schema(value_type = Object)]
    pub metadata: Value,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(nullable)]
    pub url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(nullable)]
    pub expires_at: Option<i64>,
}

#[derive(Serialize, ToSchema)]
pub struct DocumentAssetDetailResponse {
    pub id: Uuid,
    pub asset_type: String,
    pub mime_type: String,
    #[schema(value_type = Object)]
    pub metadata: Value,
    pub created_at: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    #[schema(nullable)]
    pub cardinality: Option<i32>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub objects: Vec<DocumentAssetObjectResponse>,
}

#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentVersionResponse {
    pub id: Uuid,
    pub version_number: i32,
    pub size_bytes: i64,
    pub checksum: String,
    pub created_at: String,
    #[schema(value_type = Object)]
    pub metadata: Value,
}

#[derive(Serialize, Clone, ToSchema)]
pub struct DocumentVersionDetailResponse {
    #[serde(flatten)]
    pub version: DocumentVersionResponse,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub assets: Vec<DocumentAssetResponse>,
    pub download_path: String,
}

pub fn build_download_path(
    state: &AppState,
    document: &Document,
    user_id: Uuid,
) -> AppResult<String> {
    state
        .jwt
        .generate_download_token(document.id, user_id, document.tenant_id)
        .map(|token| format!("/download/{token}"))
        .map_err(|err| {
            tracing::error!(error = ?err, "failed to generate download token");
            AppError::internal("failed to generate download token")
        })
}

pub fn to_version_response(version: DocumentVersion) -> DocumentVersionResponse {
    DocumentVersionResponse {
        id: version.id,
        version_number: version.version_number,
        size_bytes: version.size_bytes,
        checksum: version.checksum,
        created_at: to_iso(version.created_at),
        metadata: version.metadata,
    }
}

pub fn to_asset_summary(asset: DocumentAsset) -> DocumentAssetResponse {
    DocumentAssetResponse {
        id: asset.id,
        asset_type: asset.asset_type,
        mime_type: asset.mime_type,
        metadata: asset.metadata,
        cardinality: asset.cardinality,
    }
}

pub fn to_asset_detail_response(
    asset: DocumentAsset,
    objects: Vec<DocumentAssetObjectResponse>,
) -> DocumentAssetDetailResponse {
    DocumentAssetDetailResponse {
        id: asset.id,
        asset_type: asset.asset_type,
        mime_type: asset.mime_type,
        metadata: asset.metadata,
        created_at: to_iso(asset.created_at),
        cardinality: asset.cardinality,
        objects,
    }
}

pub fn to_asset_object_response(
    object: DocumentAssetObject,
    url: Option<String>,
    expires_at: Option<i64>,
) -> DocumentAssetObjectResponse {
    DocumentAssetObjectResponse {
        id: object.id,
        ordinal: object.ordinal,
        metadata: object.metadata,
        url,
        expires_at,
    }
}

pub fn delete_asset(state: &AppState, tenant_id: Uuid, asset_id: Uuid) -> AppResult<()> {
    let mut conn = state.db_for_tenant(tenant_id)?;

    diesel::delete(
        document_assets::table
            .filter(document_assets::id.eq(asset_id))
            .filter(document_assets::tenant_id.eq(tenant_id)),
    )
    .execute(&mut conn)?;

    Ok(())
}

pub async fn load_asset_responses(
    state: &AppState,
    tenant_id: Uuid,
    version_id: Uuid,
) -> AppResult<Vec<DocumentAssetResponse>> {
    let mut conn = state.db_for_tenant(tenant_id)?;
    let assets: Vec<(DocumentAsset, Option<DocumentAssetObject>)> = document_assets::table
        .left_outer_join(
            document_asset_objects::table.on(document_asset_objects::asset_id
                .eq(document_assets::id)
                .and(document_asset_objects::ordinal.eq(1))),
        )
        .filter(document_assets::document_version_id.eq(version_id))
        .filter(document_assets::tenant_id.eq(tenant_id))
        .order(document_assets::created_at.asc())
        .select((
            document_assets::all_columns,
            document_asset_objects::all_columns.nullable(),
        ))
        .load(&mut conn)?;
    drop(conn);

    Ok(assets
        .into_iter()
        .map(|(asset, _)| to_asset_summary(asset))
        .collect())
}

pub fn load_primary_assets(
    state: &AppState,
    tenant_id: Uuid,
    documents: &[Document],
) -> AppResult<HashMap<Uuid, (DocumentVersionResponse, Vec<DocumentAssetResponse>)>> {
    if documents.is_empty() {
        return Ok(HashMap::new());
    }

    let mut doc_to_version: HashMap<Uuid, Uuid> = HashMap::with_capacity(documents.len());
    let mut version_ids: Vec<Uuid> = Vec::with_capacity(documents.len());
    for doc in documents {
        doc_to_version.insert(doc.id, doc.current_version_id);
        version_ids.push(doc.current_version_id);
    }

    version_ids.sort();
    version_ids.dedup();

    let mut conn = state.db_for_tenant(tenant_id)?;
    let versions: Vec<DocumentVersion> = document_versions::table
        .filter(document_versions::id.eq_any(&version_ids))
        .load(&mut conn)?;

    let mut version_map: HashMap<Uuid, DocumentVersion> = HashMap::new();
    for version in versions {
        version_map.insert(version.id, version);
    }

    let assets: Vec<(DocumentAsset, Option<DocumentAssetObject>)> = document_assets::table
        .left_outer_join(
            document_asset_objects::table.on(document_asset_objects::asset_id
                .eq(document_assets::id)
                .and(document_asset_objects::ordinal.eq(1))),
        )
        .filter(document_assets::document_version_id.eq_any(&version_ids))
        .order((
            document_assets::document_version_id.asc(),
            document_assets::created_at.asc(),
        ))
        .select((
            document_assets::all_columns,
            document_asset_objects::all_columns.nullable(),
        ))
        .load(&mut conn)?;

    drop(conn);

    let mut assets_by_version: HashMap<Uuid, Vec<DocumentAssetResponse>> = HashMap::new();
    for (asset, _object) in assets {
        let version_id = asset.document_version_id;
        let response = to_asset_summary(asset);
        assets_by_version
            .entry(version_id)
            .or_default()
            .push(response);
    }

    let mut result: HashMap<Uuid, (DocumentVersionResponse, Vec<DocumentAssetResponse>)> =
        HashMap::with_capacity(doc_to_version.len());
    for (doc_id, version_id) in doc_to_version {
        if let Some(version) = version_map.remove(&version_id) {
            let assets = assets_by_version.remove(&version_id).unwrap_or_default();
            result.insert(doc_id, (to_version_response(version), assets));
        }
    }

    Ok(result)
}

pub fn derive_document_title(original: &str) -> String {
    let trimmed = original.trim();
    if trimmed.is_empty() {
        return "Document".to_string();
    }

    let stem = FsPath::new(trimmed)
        .file_stem()
        .and_then(|s| s.to_str())
        .map(|s| s.trim())
        .filter(|s| !s.is_empty())
        .map(|s| s.to_string());

    stem.unwrap_or_else(|| trimmed.to_string())
}

pub fn filename_with_retained_extension(title: &str, current_filename: &str) -> String {
    let extension = FsPath::new(current_filename)
        .extension()
        .and_then(|ext| ext.to_str());

    if let Some(ext) = extension {
        if title
            .rsplit_once('.')
            .map(|(_, existing_ext)| existing_ext.eq_ignore_ascii_case(ext))
            .unwrap_or(false)
        {
            title.to_string()
        } else {
            format!("{title}.{ext}")
        }
    } else {
        title.to_string()
    }
}
