use std::io::Cursor;

use image::{ImageFormat, load_from_memory};
use loco_rs::prelude::*;
use reqwest::{Url, header, redirect::Policy};
use serde::{Deserialize, Serialize};

use crate::services::{media_assets, storage, worker_jobs};

pub struct DownloadWorker {
    pub ctx: AppContext,
}

#[derive(Clone, Deserialize, Debug, Serialize)]
pub struct DownloadWorkerArgs {
    pub source_url: String,
    #[serde(default)]
    pub prefix: Option<String>,
    #[serde(default)]
    pub target_format: Option<String>,
    #[serde(default)]
    pub title: Option<String>,
    #[serde(default)]
    pub alt_text: Option<String>,
    #[serde(default)]
    pub caption: Option<String>,
    #[serde(default)]
    pub tags: Vec<String>,
    #[serde(default)]
    pub notes: Option<String>,
    #[serde(default)]
    pub job_id: Option<i32>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DownloadedMediaObject {
    pub key: String,
    pub url: String,
    pub size_bytes: i64,
    pub source_url: String,
    pub content_type: 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 normalize_prefix(value: Option<String>) -> String {
    value
        .unwrap_or_else(|| "uploads".to_string())
        .trim()
        .trim_matches('/')
        .to_string()
}

pub fn normalize_target_format(value: Option<String>) -> Result<Option<String>> {
    let Some(value) = value.map(|item| item.trim().to_ascii_lowercase()) else {
        return Ok(None);
    };

    if value.is_empty() || value == "original" {
        return Ok(None);
    }

    match value.as_str() {
        "webp" | "avif" => Ok(Some(value)),
        _ => Err(Error::BadRequest(
            "target_format 仅支持 webp、avif 或 original".to_string(),
        )),
    }
}

fn derive_file_name(url: &Url) -> Option<String> {
    url.path_segments()
        .and_then(|segments| segments.last())
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToString::to_string)
}

fn infer_extension(file_name: Option<&str>, content_type: Option<&str>) -> Option<String> {
    let from_name = file_name
        .and_then(|name| name.rsplit('.').next())
        .map(str::trim)
        .filter(|ext| !ext.is_empty())
        .map(str::to_ascii_lowercase);

    if let Some(ext) = from_name
        .as_deref()
        .filter(|ext| ext.chars().all(|ch| ch.is_ascii_alphanumeric()) && ext.len() <= 10)
    {
        return Some(ext.to_string());
    }

    match content_type
        .unwrap_or_default()
        .trim()
        .split(';')
        .next()
        .unwrap_or_default()
        .to_ascii_lowercase()
        .as_str()
    {
        "image/png" => Some("png".to_string()),
        "image/jpeg" => Some("jpg".to_string()),
        "image/webp" => Some("webp".to_string()),
        "image/gif" => Some("gif".to_string()),
        "image/avif" => Some("avif".to_string()),
        "image/svg+xml" => Some("svg".to_string()),
        "application/pdf" => Some("pdf".to_string()),
        _ => None,
    }
}

fn is_supported_content_type(value: Option<&str>) -> bool {
    value
        .unwrap_or_default()
        .trim()
        .split(';')
        .next()
        .map(|item| {
            matches!(
                item,
                "image/png"
                    | "image/jpeg"
                    | "image/webp"
                    | "image/gif"
                    | "image/avif"
                    | "image/svg+xml"
                    | "application/pdf"
            )
        })
        .unwrap_or(false)
}

fn is_convertible_bitmap_content_type(value: Option<&str>) -> bool {
    value
        .unwrap_or_default()
        .trim()
        .split(';')
        .next()
        .map(|item| {
            matches!(
                item,
                "image/png" | "image/jpeg" | "image/webp" | "image/avif"
            )
        })
        .unwrap_or(false)
}

fn target_mime_type(target_format: &str) -> Option<&'static str> {
    match target_format {
        "webp" => Some("image/webp"),
        "avif" => Some("image/avif"),
        _ => None,
    }
}

fn convert_media_bytes(
    bytes: &[u8],
    content_type: Option<&str>,
    target_format: &str,
) -> Result<(Vec<u8>, String, String)> {
    let target_mime = target_mime_type(target_format)
        .ok_or_else(|| Error::BadRequest("不支持的目标媒体格式".to_string()))?;

    if !is_convertible_bitmap_content_type(content_type) {
        return Err(Error::BadRequest(
            "当前仅支持把 PNG / JPEG / WebP / AVIF 转成 WebP 或 AVIF".to_string(),
        ));
    }

    let image = load_from_memory(bytes)
        .map_err(|error| Error::BadRequest(format!("解析远程图片失败: {error}")))?;
    let image_format = match target_format {
        "webp" => ImageFormat::WebP,
        "avif" => ImageFormat::Avif,
        _ => return Err(Error::BadRequest("不支持的目标媒体格式".to_string())),
    };

    let mut cursor = Cursor::new(Vec::new());
    image
        .write_to(&mut cursor, image_format)
        .map_err(|error| Error::BadRequest(format!("转换远程图片格式失败: {error}")))?;

    Ok((
        cursor.into_inner(),
        target_format.to_string(),
        target_mime.to_string(),
    ))
}

fn default_title(args: &DownloadWorkerArgs, file_name: Option<&str>) -> String {
    trim_to_option(args.title.clone())
        .or_else(|| {
            file_name.map(|value| {
                value
                    .rsplit_once('.')
                    .map(|(stem, _)| stem)
                    .unwrap_or(value)
                    .replace(['-', '_'], " ")
                    .trim()
                    .to_string()
            })
        })
        .filter(|value| !value.is_empty())
        .unwrap_or_else(|| "remote asset".to_string())
}

fn merge_notes(notes: Option<String>, source_url: &str) -> Option<String> {
    let note = notes.unwrap_or_default().trim().to_string();
    let source_line = format!("source_url: {source_url}");

    if note.is_empty() {
        return Some(source_line);
    }

    if note.contains(&source_line) {
        return Some(note);
    }

    Some(format!("{note}\n{source_line}"))
}

pub async fn download_media_to_storage(
    ctx: &AppContext,
    args: &DownloadWorkerArgs,
) -> Result<DownloadedMediaObject> {
    let source_url = trim_to_option(Some(args.source_url.clone()))
        .ok_or_else(|| Error::BadRequest("source_url 不能为空".to_string()))?;
    let parsed_url = Url::parse(&source_url)
        .map_err(|_| Error::BadRequest("source_url 必须是合法的绝对 URL".to_string()))?;

    let client = reqwest::Client::builder()
        .redirect(Policy::limited(5))
        .build()
        .map_err(|error| Error::BadRequest(format!("初始化下载客户端失败: {error}")))?;

    let response = client
        .get(parsed_url.clone())
        .send()
        .await
        .map_err(|error| Error::BadRequest(format!("下载远程媒体失败: {error}")))?;

    if !response.status().is_success() {
        return Err(Error::BadRequest(format!(
            "下载远程媒体失败，状态码：{}",
            response.status()
        )));
    }

    let final_url = response.url().clone();
    let content_type = response
        .headers()
        .get(header::CONTENT_TYPE)
        .and_then(|value| value.to_str().ok())
        .map(ToString::to_string);

    if !is_supported_content_type(content_type.as_deref()) {
        return Err(Error::BadRequest(
            "仅支持图片或 PDF 资源的远程抓取".to_string(),
        ));
    }

    let bytes = response
        .bytes()
        .await
        .map_err(|error| Error::BadRequest(format!("读取远程媒体内容失败: {error}")))?;

    if bytes.is_empty() {
        return Err(Error::BadRequest("下载到的远程媒体内容为空".to_string()));
    }

    let file_name = derive_file_name(&final_url);
    let target_format = normalize_target_format(args.target_format.clone())?;
    let normalized_source_content_type = content_type
        .as_deref()
        .map(str::trim)
        .and_then(|value| value.split(';').next())
        .map(str::to_ascii_lowercase);
    let already_target_format = target_format
        .as_deref()
        .and_then(target_mime_type)
        .zip(normalized_source_content_type.as_deref())
        .map(|(target_mime, source_mime)| source_mime == target_mime)
        .unwrap_or(false);
    let (payload_bytes, extension, resolved_content_type) =
        if let Some(target_format) = target_format.as_deref() {
            if already_target_format {
                (
                    bytes.to_vec(),
                    target_format.to_string(),
                    target_mime_type(target_format)
                        .unwrap_or_default()
                        .to_string(),
                )
            } else {
                convert_media_bytes(&bytes, content_type.as_deref(), target_format)?
            }
        } else {
            (
                bytes.to_vec(),
                infer_extension(file_name.as_deref(), content_type.as_deref())
                    .ok_or_else(|| Error::BadRequest("无法识别远程媒体文件类型".to_string()))?,
                content_type
                    .clone()
                    .unwrap_or_else(|| "application/octet-stream".to_string()),
            )
        };
    let prefix = normalize_prefix(args.prefix.clone());
    let object_key = storage::build_object_key(
        &prefix,
        &default_title(args, file_name.as_deref()),
        &extension,
    );
    let stored = storage::upload_bytes_to_r2(
        ctx,
        &object_key,
        payload_bytes.clone(),
        Some(resolved_content_type.as_str()),
        Some("public, max-age=31536000, immutable"),
    )
    .await?;

    media_assets::upsert_by_key(
        ctx,
        &stored.key,
        media_assets::MediaAssetMetadataInput {
            title: trim_to_option(args.title.clone())
                .or_else(|| trim_to_option(Some(default_title(args, file_name.as_deref())))),
            alt_text: trim_to_option(args.alt_text.clone()),
            caption: trim_to_option(args.caption.clone()),
            tags: (!args.tags.is_empty()).then_some(args.tags.clone()),
            notes: merge_notes(args.notes.clone(), final_url.as_str()),
        },
    )
    .await?;

    Ok(DownloadedMediaObject {
        key: stored.key,
        url: stored.url,
        size_bytes: payload_bytes.len() as i64,
        source_url: final_url.to_string(),
        content_type: Some(resolved_content_type),
    })
}

#[async_trait]
impl BackgroundWorker<DownloadWorkerArgs> for DownloadWorker {
    fn build(ctx: &AppContext) -> Self {
        Self { ctx: ctx.clone() }
    }

    async fn perform(&self, args: DownloadWorkerArgs) -> Result<()> {
        if let Some(job_id) = args.job_id {
            if !worker_jobs::begin_job_execution(&self.ctx, job_id).await? {
                return Ok(());
            }

            match download_media_to_storage(&self.ctx, &args).await {
                Ok(downloaded) => {
                    worker_jobs::mark_job_succeeded(
                        &self.ctx,
                        job_id,
                        Some(serde_json::to_value(downloaded)?),
                    )
                    .await?;
                    Ok(())
                }
                Err(error) => {
                    worker_jobs::mark_job_failed(&self.ctx, job_id, error.to_string()).await?;
                    Err(error)
                }
            }
        } else {
            download_media_to_storage(&self.ctx, &args).await?;
            Ok(())
        }
    }
}
