use aws_config::BehaviorVersion;
use aws_sdk_s3::{Client, config::Credentials, primitives::ByteStream};
use loco_rs::prelude::*;
use sea_orm::{EntityTrait, QueryOrder};
use std::path::{Path, PathBuf};
use uuid::Uuid;

use crate::models::_entities::site_settings;

const ENV_MEDIA_PROVIDER: &str = "TERMI_MEDIA_PROVIDER";
const ENV_MEDIA_ENDPOINT: &str = "TERMI_MEDIA_ENDPOINT";
const ENV_MEDIA_BUCKET: &str = "TERMI_MEDIA_BUCKET";
const ENV_MEDIA_PUBLIC_BASE_URL: &str = "TERMI_MEDIA_PUBLIC_BASE_URL";
const ENV_MEDIA_ACCESS_KEY_ID: &str = "TERMI_MEDIA_ACCESS_KEY_ID";
const ENV_MEDIA_SECRET_ACCESS_KEY: &str = "TERMI_MEDIA_SECRET_ACCESS_KEY";

const ENV_R2_ACCOUNT_ID: &str = "TERMI_R2_ACCOUNT_ID";
const ENV_R2_BUCKET: &str = "TERMI_R2_BUCKET";
const ENV_R2_PUBLIC_BASE_URL: &str = "TERMI_R2_PUBLIC_BASE_URL";
const ENV_R2_ACCESS_KEY_ID: &str = "TERMI_R2_ACCESS_KEY_ID";
const ENV_R2_SECRET_ACCESS_KEY: &str = "TERMI_R2_SECRET_ACCESS_KEY";

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MediaStorageProvider {
    R2,
    Minio,
}

#[derive(Clone, Debug)]
pub struct MediaStorageSettings {
    pub provider: MediaStorageProvider,
    pub provider_name: String,
    pub endpoint: String,
    pub bucket: String,
    pub public_base_url: String,
    pub access_key_id: String,
    pub secret_access_key: String,
    pub region: String,
    pub force_path_style: bool,
}

#[derive(Clone, Debug)]
pub struct StoredObject {
    pub key: String,
    pub url: String,
}

#[derive(Clone, Debug)]
pub struct StoredObjectSummary {
    pub key: String,
    pub url: String,
    pub size_bytes: i64,
    pub last_modified: Option<String>,
}

fn trim_to_option(value: Option<String>) -> Option<String> {
    value.and_then(|item| {
        let trimmed = item.trim().to_string();
        if trimmed.is_empty() {
            None
        } else {
            Some(trimmed)
        }
    })
}

fn env_value(name: &str) -> Option<String> {
    std::env::var(name)
        .ok()
        .and_then(|value| trim_to_option(Some(value)))
}

fn slugify_segment(value: &str) -> String {
    let mut output = String::new();
    let mut previous_dash = false;

    for ch in value.chars() {
        if ch.is_ascii_alphanumeric() {
            output.push(ch.to_ascii_lowercase());
            previous_dash = false;
        } else if !previous_dash {
            output.push('-');
            previous_dash = true;
        }
    }

    output.trim_matches('-').to_string()
}

fn normalize_public_base_url(value: String) -> String {
    value.trim().trim_end_matches('/').to_string()
}

fn normalize_provider(value: Option<String>) -> MediaStorageProvider {
    match value
        .as_deref()
        .map(str::trim)
        .unwrap_or_default()
        .to_ascii_lowercase()
        .as_str()
    {
        "minio" => MediaStorageProvider::Minio,
        _ => MediaStorageProvider::R2,
    }
}

async fn load_settings_row(ctx: &AppContext) -> Result<Option<site_settings::Model>> {
    site_settings::Entity::find()
        .order_by_asc(site_settings::Column::Id)
        .one(&ctx.db)
        .await
        .map_err(Into::into)
}

fn build_r2_endpoint(account_id: &str) -> String {
    let trimmed = account_id.trim().trim_end_matches('/');
    if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
        trimmed.to_string()
    } else {
        format!("https://{trimmed}.r2.cloudflarestorage.com")
    }
}

fn default_public_base_url_for_minio(endpoint: &str, bucket: &str) -> String {
    format!(
        "{}/{}",
        endpoint.trim().trim_end_matches('/'),
        bucket.trim().trim_start_matches('/')
    )
}

pub fn generated_cover_directory() -> PathBuf {
    let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let candidates = [
        current_dir
            .join("frontend")
            .join("public")
            .join("generated-covers"),
        current_dir
            .join("..")
            .join("frontend")
            .join("public")
            .join("generated-covers"),
    ];

    candidates
        .into_iter()
        .find(|path| path.parent().map(|parent| parent.exists()).unwrap_or(false))
        .unwrap_or_else(|| {
            PathBuf::from("..")
                .join("frontend")
                .join("public")
                .join("generated-covers")
        })
}

pub fn file_extension_for_path(path: &Path) -> &str {
    path.extension()
        .and_then(|ext| ext.to_str())
        .map(str::trim)
        .filter(|ext| !ext.is_empty())
        .unwrap_or("png")
}

pub async fn optional_r2_settings(ctx: &AppContext) -> Result<Option<MediaStorageSettings>> {
    let row = load_settings_row(ctx).await?;

    let provider_raw = row
        .as_ref()
        .and_then(|item| trim_to_option(item.media_storage_provider.clone()))
        .or_else(|| env_value(ENV_MEDIA_PROVIDER));
    let provider = normalize_provider(provider_raw.clone());

    let endpoint_or_account = row
        .as_ref()
        .and_then(|item| trim_to_option(item.media_r2_account_id.clone()))
        .or_else(|| env_value(ENV_MEDIA_ENDPOINT))
        .or_else(|| env_value(ENV_R2_ACCOUNT_ID));
    let bucket = row
        .as_ref()
        .and_then(|item| trim_to_option(item.media_r2_bucket.clone()))
        .or_else(|| env_value(ENV_MEDIA_BUCKET))
        .or_else(|| env_value(ENV_R2_BUCKET));
    let access_key_id = row
        .as_ref()
        .and_then(|item| trim_to_option(item.media_r2_access_key_id.clone()))
        .or_else(|| env_value(ENV_MEDIA_ACCESS_KEY_ID))
        .or_else(|| env_value(ENV_R2_ACCESS_KEY_ID));
    let secret_access_key = row
        .as_ref()
        .and_then(|item| trim_to_option(item.media_r2_secret_access_key.clone()))
        .or_else(|| env_value(ENV_MEDIA_SECRET_ACCESS_KEY))
        .or_else(|| env_value(ENV_R2_SECRET_ACCESS_KEY));

    let public_base_url = row
        .as_ref()
        .and_then(|item| trim_to_option(item.media_r2_public_base_url.clone()))
        .or_else(|| env_value(ENV_MEDIA_PUBLIC_BASE_URL))
        .or_else(|| env_value(ENV_R2_PUBLIC_BASE_URL))
        .or_else(
            || match (&provider, endpoint_or_account.as_deref(), bucket.as_deref()) {
                (MediaStorageProvider::Minio, Some(endpoint), Some(bucket)) => {
                    Some(default_public_base_url_for_minio(endpoint, bucket))
                }
                _ => None,
            },
        );

    let has_any = endpoint_or_account.is_some()
        || bucket.is_some()
        || public_base_url.is_some()
        || access_key_id.is_some()
        || secret_access_key.is_some()
        || provider_raw.is_some();

    if !has_any {
        return Ok(None);
    }

    let missing = [
        (
            match provider {
                MediaStorageProvider::Minio => "Endpoint",
                MediaStorageProvider::R2 => "Account ID",
            },
            endpoint_or_account.is_some(),
        ),
        ("Bucket", bucket.is_some()),
        ("Public Base URL", public_base_url.is_some()),
        ("Access Key ID", access_key_id.is_some()),
        ("Secret Access Key", secret_access_key.is_some()),
    ]
    .into_iter()
    .filter_map(|(label, present)| (!present).then_some(label))
    .collect::<Vec<_>>();

    if !missing.is_empty() {
        return Err(Error::BadRequest(format!(
            "对象存储配置不完整，请补齐：{}",
            missing.join(" / ")
        )));
    }

    let endpoint = match provider {
        MediaStorageProvider::R2 => {
            build_r2_endpoint(&endpoint_or_account.clone().unwrap_or_default())
        }
        MediaStorageProvider::Minio => endpoint_or_account.clone().unwrap_or_default(),
    };

    Ok(Some(MediaStorageSettings {
        provider: provider.clone(),
        provider_name: match provider {
            MediaStorageProvider::R2 => "r2".to_string(),
            MediaStorageProvider::Minio => "minio".to_string(),
        },
        endpoint,
        bucket: bucket.unwrap_or_default(),
        public_base_url: normalize_public_base_url(public_base_url.unwrap_or_default()),
        access_key_id: access_key_id.unwrap_or_default(),
        secret_access_key: secret_access_key.unwrap_or_default(),
        region: match provider {
            MediaStorageProvider::R2 => "auto".to_string(),
            MediaStorageProvider::Minio => "us-east-1".to_string(),
        },
        force_path_style: provider == MediaStorageProvider::Minio,
    }))
}

pub async fn require_r2_settings(ctx: &AppContext) -> Result<MediaStorageSettings> {
    optional_r2_settings(ctx)
        .await?
        .ok_or_else(|| Error::BadRequest("请先在后台配置媒体对象存储".to_string()))
}

async fn r2_client(settings: &MediaStorageSettings) -> Client {
    let shared_config = aws_config::defaults(BehaviorVersion::latest())
        .endpoint_url(settings.endpoint.clone())
        .credentials_provider(Credentials::new(
            settings.access_key_id.clone(),
            settings.secret_access_key.clone(),
            None,
            None,
            match settings.provider {
                MediaStorageProvider::R2 => "r2",
                MediaStorageProvider::Minio => "minio",
            },
        ))
        .region(aws_sdk_s3::config::Region::new(settings.region.clone()))
        .load()
        .await;

    let conf = aws_sdk_s3::config::Builder::from(&shared_config)
        .force_path_style(settings.force_path_style)
        .build();

    Client::from_conf(conf)
}

fn build_public_url(settings: &MediaStorageSettings, key: &str) -> String {
    format!(
        "{}/{}",
        settings.public_base_url,
        key.trim_start_matches('/')
    )
}

pub fn object_key_from_public_url(settings: &MediaStorageSettings, url: &str) -> Option<String> {
    let normalized_base = settings.public_base_url.trim().trim_end_matches('/');
    let normalized_url = url.trim();

    if normalized_base.is_empty() || normalized_url.is_empty() {
        return None;
    }

    normalized_url
        .strip_prefix(normalized_base)
        .map(|suffix| suffix.trim_start_matches('/').to_string())
        .filter(|suffix| !suffix.is_empty())
}

pub fn build_object_key(prefix: &str, stem: &str, extension: &str) -> String {
    let safe_prefix = prefix.trim_matches('/');
    let safe_stem = slugify_segment(stem);
    let safe_extension = extension
        .trim()
        .trim_start_matches('.')
        .to_ascii_lowercase();
    let object_name = format!(
        "{}-{}.{}",
        if safe_stem.is_empty() {
            "asset".to_string()
        } else {
            safe_stem
        },
        Uuid::new_v4().simple(),
        if safe_extension.is_empty() {
            "bin".to_string()
        } else {
            safe_extension
        },
    );

    if safe_prefix.is_empty() {
        object_name
    } else {
        format!("{safe_prefix}/{object_name}")
    }
}

async fn ensure_bucket_exists(client: &Client, settings: &MediaStorageSettings) -> Result<()> {
    if client
        .head_bucket()
        .bucket(&settings.bucket)
        .send()
        .await
        .is_ok()
    {
        return Ok(());
    }

    if settings.provider != MediaStorageProvider::Minio {
        return Err(Error::BadRequest(format!(
            "对象存储 bucket 不存在或不可访问：{}",
            settings.bucket
        )));
    }

    client
        .create_bucket()
        .bucket(&settings.bucket)
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("自动创建 MinIO bucket 失败: {error}")))?;

    Ok(())
}

pub async fn upload_bytes_to_r2(
    ctx: &AppContext,
    key: &str,
    bytes: Vec<u8>,
    content_type: Option<&str>,
    cache_control: Option<&str>,
) -> Result<StoredObject> {
    let settings = require_r2_settings(ctx).await?;
    let client = r2_client(&settings).await;
    ensure_bucket_exists(&client, &settings).await?;

    let mut request = client
        .put_object()
        .bucket(&settings.bucket)
        .key(key)
        .body(ByteStream::from(bytes));

    if let Some(content_type) = content_type
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        request = request.content_type(content_type);
    }

    if let Some(cache_control) = cache_control
        .map(str::trim)
        .filter(|value| !value.is_empty())
    {
        request = request.cache_control(cache_control);
    }

    request
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("上传文件到对象存储失败: {error}")))?;

    Ok(StoredObject {
        key: key.to_string(),
        url: build_public_url(&settings, key),
    })
}

pub async fn delete_object(ctx: &AppContext, key: &str) -> Result<()> {
    let settings = require_r2_settings(ctx).await?;
    let client = r2_client(&settings).await;
    client
        .delete_object()
        .bucket(&settings.bucket)
        .key(key)
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("删除对象存储文件失败: {error}")))?;
    Ok(())
}

pub async fn delete_managed_url(ctx: &AppContext, url: &str) -> Result<bool> {
    let Some(settings) = optional_r2_settings(ctx).await? else {
        return Ok(false);
    };
    let Some(key) = object_key_from_public_url(&settings, url) else {
        return Ok(false);
    };
    let client = r2_client(&settings).await;
    client
        .delete_object()
        .bucket(&settings.bucket)
        .key(&key)
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("删除对象存储文件失败: {error}")))?;
    Ok(true)
}

pub async fn list_objects(
    ctx: &AppContext,
    prefix: Option<&str>,
    limit: i32,
) -> Result<Vec<StoredObjectSummary>> {
    let settings = require_r2_settings(ctx).await?;
    let client = r2_client(&settings).await;
    ensure_bucket_exists(&client, &settings).await?;

    let mut request = client
        .list_objects_v2()
        .bucket(&settings.bucket)
        .max_keys(limit.clamp(1, 1000));

    if let Some(prefix) = prefix.map(str::trim).filter(|value| !value.is_empty()) {
        request = request.prefix(prefix);
    }

    let result = request
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("读取对象存储列表失败: {error}")))?;

    Ok(result
        .contents()
        .iter()
        .filter_map(|item| {
            let key = item.key()?.to_string();
            Some(StoredObjectSummary {
                url: build_public_url(&settings, &key),
                key,
                size_bytes: item.size().unwrap_or_default(),
                last_modified: item.last_modified().map(|ts| format!("{ts:?}")),
            })
        })
        .collect())
}

pub async fn test_r2_connectivity(ctx: &AppContext) -> Result<String> {
    let settings = require_r2_settings(ctx).await?;
    let client = r2_client(&settings).await;
    ensure_bucket_exists(&client, &settings).await?;
    let healthcheck_key = format!(".healthchecks/{}.txt", Uuid::new_v4().simple());
    client
        .put_object()
        .bucket(&settings.bucket)
        .key(&healthcheck_key)
        .body(ByteStream::from_static(b"termi-storage-ok"))
        .content_type("text/plain")
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("对象存储连接测试失败: {error}")))?;
    client
        .delete_object()
        .bucket(&settings.bucket)
        .key(&healthcheck_key)
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("对象存储清理测试文件失败: {error}")))?;

    Ok(settings.bucket)
}
