use reqwest::Method;
use serde::Deserialize;
use serde_json::{json, Map, Value};
use std::path::Path;

use crate::error::Result;
use crate::transport::HttpTransport;
use crate::types::{
    AssetDeleteResponse, AssetListResponse, AssetType, AudioOptions, BatchGenerateRequest,
    BatchGenerateResponse, CharacterOptions, GenerateRequest, GenerateResponse, ImageOptions,
    JobCancelResponse, JobStatusResponse, JobSummary, LibraryAddRequest, LibraryAddResponse,
    LibraryCatalogJobsRequest, LibraryCatalogJobsResponse, LibraryDeleteResponse, LibraryItem,
    LibrarySearchResponse, LoginResponse, Model3dOptions, MusicOptions, Process3dRequest,
    Process3dResponse, ProcessRequest, ProcessResponse, PropOptions, ProviderHealthListResponse,
    ProviderHealthResponse, ProviderInfo, SpriteOptions, TextOptions, TtsOptions, UploadResult,
    VideoOptions, VoiceCloneRequest, VoiceCloneResponse, VoiceDeleteResponse, VoiceDesignRequest,
    VoiceDesignResponse, VoiceListResponse, VoiceType, WorldOptions,
};

/// Default Asset Gateway endpoint used by [`AssetClient`].
pub const DEFAULT_BASE_URL: &str = "https://asset.origingame.dev";

/// Builder for configuring an [`AssetClient`].
#[derive(Debug, Clone)]
pub struct AssetClientBuilder {
    api_key: String,
    base_url: String,
    client: Option<reqwest::Client>,
}

impl AssetClientBuilder {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            api_key: api_key.into(),
            base_url: DEFAULT_BASE_URL.to_string(),
            client: None,
        }
    }

    /// Override the Asset Gateway base URL.
    pub fn base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = url.into();
        self
    }

    /// Use a custom `reqwest::Client` (e.g. custom timeout, proxy, TLS config).
    pub fn http_client(mut self, client: reqwest::Client) -> Self {
        self.client = Some(client);
        self
    }

    pub fn build(self) -> AssetClient {
        let transport = HttpTransport::new(self.base_url, self.api_key);
        let transport = if let Some(client) = self.client {
            transport.with_client(client)
        } else {
            transport
        };

        AssetClient { transport }
    }
}

/// Client for the Origin Asset Gateway service.
#[derive(Debug, Clone)]
pub struct AssetClient {
    pub(crate) transport: HttpTransport,
}

impl AssetClient {
    /// Create a client with the default Asset Gateway base URL.
    pub fn new(api_key: impl Into<String>) -> Self {
        AssetClientBuilder::new(api_key).build()
    }

    /// Create a builder for fine-grained configuration.
    pub fn builder(api_key: impl Into<String>) -> AssetClientBuilder {
        AssetClientBuilder::new(api_key)
    }

    /// Return the configured Asset Gateway base URL.
    pub fn base_url(&self) -> &str {
        self.transport.base_url()
    }

    pub async fn generate(&self, request: &GenerateRequest) -> Result<GenerateResponse> {
        let body = json!({
            "asset_type": request.asset_type,
            "prompt": request.prompt,
            "model": request.model,
            "input_file": request.input_file,
            "provider": request.provider,
            "size": request.size,
            "transparent": request.transparent,
            "reference_images": request.reference_images,
            "edit_mode": request.edit_mode,
            "params": request.params,
        });

        self.transport.post("/api/generate", &body).await
    }

    pub async fn generate_image(
        &self,
        prompt: impl Into<String>,
        options: Option<ImageOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Image,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: options.size,
            transparent: options.transparent,
            reference_images: options.reference_images,
            edit_mode: options.edit_mode,
            session_id: options.session_id,
            params: options.params,
        };

        self.generate(&request).await
    }

    pub async fn generate_video(
        &self,
        prompt: impl Into<String>,
        options: Option<VideoOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let mut params = options.params;
        if let Some(ratio) = &options.ratio {
            if let Value::Object(ref mut map) = params {
                map.insert("ratio".to_string(), Value::String(ratio.clone()));
            } else {
                params = serde_json::json!({ "ratio": ratio });
            }
        }
        let request = GenerateRequest {
            asset_type: AssetType::Video,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: options.size,
            transparent: None,
            reference_images: options.reference_images,
            edit_mode: None,
            session_id: None,
            params,
        };

        self.generate(&request).await
    }

    pub async fn generate_audio(
        &self,
        prompt: impl Into<String>,
        options: Option<AudioOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Audio,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: None,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry("audio_type", options.audio_type),
                    option_entry("duration_seconds", options.duration.map(Value::from)),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_tts(
        &self,
        prompt: impl Into<String>,
        options: Option<TtsOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Tts,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: None,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry("voice", options.voice),
                    option_entry("voice_id", options.voice_id),
                    option_entry("language_type", options.language),
                    option_entry("instructions", options.instructions),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_music(
        &self,
        prompt: impl Into<String>,
        options: Option<MusicOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Music,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: None,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry("duration_seconds", options.duration.map(Value::from)),
                    bool_entry("force_instrumental", options.force_instrumental),
                    option_entry("output_format", options.output_format),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_model3d(
        &self,
        prompt: impl Into<String>,
        options: Option<Model3dOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Model3d,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry("ai_model", options.ai_model),
                    option_entry("polycount", options.polycount.map(Value::from)),
                    bool_entry("pbr", options.pbr),
                    bool_entry("hd_texture", options.hd_texture),
                    bool_entry("auto_size", options.auto_size),
                    option_entry("pose_mode", options.pose_mode),
                    option_entry("texture_prompt", options.texture_prompt),
                    option_entry("format", options.format),
                    option_entry(
                        "images",
                        (!options.images.is_empty()).then(|| {
                            Value::Array(options.images.into_iter().map(Value::from).collect())
                        }),
                    ),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_character(
        &self,
        prompt: impl Into<String>,
        options: Option<CharacterOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Character,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry("format", options.format),
                    option_entry("polycount", options.polycount.map(Value::from)),
                    bool_entry("pbr", options.pbr),
                    bool_entry("hd_texture", options.hd_texture),
                    option_entry("pose_mode", options.pose_mode),
                    option_entry("ai_model", options.ai_model),
                    bool_entry("auto_size", options.auto_size),
                    option_entry("texture_prompt", options.texture_prompt),
                    option_entry(
                        "images",
                        (!options.images.is_empty()).then(|| {
                            Value::Array(options.images.into_iter().map(Value::from).collect())
                        }),
                    ),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_prop(
        &self,
        prompt: impl Into<String>,
        options: Option<PropOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Prop,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry("format", options.format),
                    option_entry("polycount", options.polycount.map(Value::from)),
                    bool_entry("pbr", options.pbr),
                    bool_entry("hd_texture", options.hd_texture),
                    option_entry("ai_model", options.ai_model),
                    bool_entry("auto_size", options.auto_size),
                    option_entry("texture_prompt", options.texture_prompt),
                    option_entry("symmetry_mode", options.symmetry_mode),
                    option_entry(
                        "images",
                        (!options.images.is_empty()).then(|| {
                            Value::Array(options.images.into_iter().map(Value::from).collect())
                        }),
                    ),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_sprite(
        &self,
        prompt: impl Into<String>,
        options: Option<SpriteOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Sprite,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [
                    option_entry(
                        "animation_type",
                        Some(Value::String(
                            options.animation_type.unwrap_or_else(|| "walk".to_string()),
                        )),
                    ),
                    option_entry(
                        "direction",
                        Some(Value::String(
                            options.direction.unwrap_or_else(|| "right".to_string()),
                        )),
                    ),
                    option_entry("duration", Some(Value::from(options.duration.unwrap_or(2)))),
                    option_entry(
                        "output_format",
                        Some(Value::String(
                            options
                                .output_format
                                .unwrap_or_else(|| "spritesheet".to_string()),
                        )),
                    ),
                    option_entry("fps", options.fps.map(Value::from)),
                    option_entry("style", options.style),
                ],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_world(
        &self,
        prompt: impl Into<String>,
        options: Option<WorldOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::World,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: options.input,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [option_entry("display_name", options.display_name)],
            ),
        };

        self.generate(&request).await
    }

    pub async fn generate_text(
        &self,
        prompt: impl Into<String>,
        options: Option<TextOptions>,
    ) -> Result<GenerateResponse> {
        let options = options.unwrap_or_default();
        let request = GenerateRequest {
            asset_type: AssetType::Text,
            prompt: Some(prompt.into()),
            model: options.model,
            input_file: None,
            provider: options.provider,
            size: None,
            transparent: None,
            reference_images: Vec::new(),
            edit_mode: None,
            session_id: None,

            params: merge_params(
                options.params,
                [option_entry(
                    "max_tokens",
                    options.max_tokens.map(Value::from),
                )],
            ),
        };

        self.generate(&request).await
    }

    pub async fn batch(&self, request: &BatchGenerateRequest) -> Result<BatchGenerateResponse> {
        let body = json!({
            "asset_type": request.asset_type,
            "prompts": request.prompts,
            "shared": request.shared,
            "compose": request.compose,
        });

        self.transport.post("/api/generate/batch", &body).await
    }

    pub async fn process(&self, request: &ProcessRequest) -> Result<ProcessResponse> {
        let body = json!({
            "input": request.input,
            "inputs": request.inputs,
            "operations": request.operations,
        });

        self.transport.post("/api/process", &body).await
    }

    pub async fn process3d(&self, request: &Process3dRequest) -> Result<Process3dResponse> {
        let body = json!({
            "task_id": request.task_id,
            "operation": request.operation,
            "params": request.params,
            "frame_count": request.frame_count,
            "resolution": request.resolution,
            "camera_angle": request.camera_angle,
            "directions": request.directions,
        });

        self.transport.post("/api/process3d", &body).await
    }

    pub async fn voice_clone(&self, request: &VoiceCloneRequest) -> Result<VoiceCloneResponse> {
        let body = serde_json::to_value(request)?;
        self.transport.post("/api/voice/clone", &body).await
    }

    pub async fn voice_design(&self, request: &VoiceDesignRequest) -> Result<VoiceDesignResponse> {
        let body = serde_json::to_value(request)?;
        self.transport.post("/api/voice/design", &body).await
    }

    pub async fn voice_list(
        &self,
        voice_type: Option<VoiceType>,
        page: Option<u32>,
        page_size: Option<u32>,
    ) -> Result<VoiceListResponse> {
        let mut path = String::from("/api/voice/list");
        let mut has_query = false;

        if let Some(voice_type) = voice_type {
            push_query_param(&mut path, &mut has_query, "type", voice_type.as_str());
        }
        if let Some(page) = page {
            push_query_param(&mut path, &mut has_query, "page", &page.to_string());
        }
        if let Some(page_size) = page_size {
            push_query_param(
                &mut path,
                &mut has_query,
                "page_size",
                &page_size.to_string(),
            );
        }

        self.transport.get(&path).await
    }

    pub async fn voice_delete(
        &self,
        voice_id: &str,
        voice_type: Option<VoiceType>,
    ) -> Result<VoiceDeleteResponse> {
        let mut path = format!("/api/voice/{}", percent_encode(voice_id));
        let mut has_query = false;

        if let Some(voice_type) = voice_type {
            push_query_param(&mut path, &mut has_query, "type", voice_type.as_str());
        }

        self.transport.delete(&path).await
    }

    pub async fn upload_file(&self, file_path: impl AsRef<Path>) -> Result<UploadResult> {
        let file_path = file_path.as_ref();
        let bytes = tokio::fs::read(file_path).await?;
        let filename = file_path
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("upload")
            .to_string();
        let part = reqwest::multipart::Part::bytes(bytes).file_name(filename);
        let form = reqwest::multipart::Form::new().part("file", part);

        self.transport
            .post_multipart("/api/assets/upload", form)
            .await
    }

    pub async fn list_assets(&self) -> Result<AssetListResponse> {
        self.transport.get("/api/assets").await
    }

    pub async fn delete_asset(&self, filename: &str) -> Result<AssetDeleteResponse> {
        self.transport
            .delete(&format!("/api/assets/{}", percent_encode(filename)))
            .await
    }

    pub async fn library_search(
        &self,
        query: Option<&str>,
        asset_type: Option<&str>,
        tags: Option<&str>,
        source: Option<&str>,
        limit: Option<i64>,
        offset: Option<i64>,
    ) -> Result<LibrarySearchResponse> {
        let mut path = String::from("/api/library/search");
        let mut has_query = false;

        if let Some(query) = query {
            push_query_param(&mut path, &mut has_query, "q", query);
        }
        if let Some(asset_type) = asset_type {
            push_query_param(&mut path, &mut has_query, "type", asset_type);
        }
        if let Some(tags) = tags {
            if !tags.trim().is_empty() {
                push_query_param(&mut path, &mut has_query, "tags", tags);
            }
        }
        if let Some(source) = source {
            push_query_param(&mut path, &mut has_query, "source", source);
        }
        if let Some(limit) = limit {
            push_query_param(&mut path, &mut has_query, "limit", &limit.to_string());
        }
        if let Some(offset) = offset {
            push_query_param(&mut path, &mut has_query, "offset", &offset.to_string());
        }

        self.transport.get(&path).await
    }

    pub async fn library_add(&self, request: &LibraryAddRequest) -> Result<LibraryAddResponse> {
        let body = serde_json::to_value(request)?;
        self.transport.post("/api/library", &body).await
    }

    pub async fn library_get(&self, id: &str) -> Result<LibraryItem> {
        self.transport
            .get(&format!("/api/library/{}", percent_encode(id)))
            .await
    }

    pub async fn library_delete(&self, id: &str) -> Result<LibraryDeleteResponse> {
        self.transport
            .delete(&format!("/api/library/{}", percent_encode(id)))
            .await
    }

    pub async fn library_catalog_jobs(
        &self,
        request: &LibraryCatalogJobsRequest,
    ) -> Result<LibraryCatalogJobsResponse> {
        let body = serde_json::to_value(request)?;
        self.transport
            .post("/api/library/catalog-jobs", &body)
            .await
    }

    pub async fn jobs(&self, status: Option<&str>, limit: Option<u32>) -> Result<Vec<JobSummary>> {
        let mut path = String::from("/api/jobs");
        let mut query = Vec::new();

        if let Some(status) = status {
            query.push(format!("status={status}"));
        }
        if let Some(limit) = limit {
            query.push(format!("limit={limit}"));
        }
        if !query.is_empty() {
            path.push('?');
            path.push_str(&query.join("&"));
        }

        let response: JobListResponse = self.transport.get(&path).await?;
        Ok(response.jobs)
    }

    pub async fn job_status(&self, job_id: &str) -> Result<JobSummary> {
        self.transport.get(&format!("/api/jobs/{job_id}")).await
    }

    pub async fn providers(&self) -> Result<Vec<ProviderInfo>> {
        let response: ProviderListResponse = self.transport.get("/api/providers").await?;
        Ok(response.providers)
    }

    pub async fn health(&self) -> Result<bool> {
        let response: HealthResponse = self.transport.request(Method::GET, "/health", None).await?;
        Ok(response.status == "healthy")
    }

    pub async fn login(&self, token: Option<&str>) -> Result<LoginResponse> {
        let tok = token
            .map(String::from)
            .unwrap_or_else(|| self.transport.api_key().to_string());
        let body = json!({ "token": tok });
        self.transport.post("/auth/login", &body).await
    }

    pub async fn job_cancel(&self, job_id: &str) -> Result<JobCancelResponse> {
        self.transport
            .post(
                &format!("/api/jobs/{}/cancel", percent_encode(job_id)),
                &json!({}),
            )
            .await
    }

    pub async fn job_wait(
        &self,
        job_id: &str,
        interval_ms: Option<u64>,
    ) -> Result<JobStatusResponse> {
        let interval = std::time::Duration::from_millis(interval_ms.unwrap_or(1000).max(250));
        loop {
            let job: JobStatusResponse = self
                .transport
                .get(&format!("/api/jobs/{}", percent_encode(job_id)))
                .await?;
            match job.status.as_str() {
                "completed" | "failed" | "cancelled" => return Ok(job),
                _ => tokio::time::sleep(interval).await,
            }
        }
    }

    pub async fn provider_health(&self) -> Result<ProviderHealthListResponse> {
        self.transport.get("/api/providers/health").await
    }

    pub async fn provider_health_single(&self, id: &str) -> Result<ProviderHealthResponse> {
        self.transport
            .get(&format!("/api/providers/{}/health", percent_encode(id)))
            .await
    }
}

#[derive(Debug, Deserialize)]
struct JobListResponse {
    jobs: Vec<JobSummary>,
}

#[derive(Debug, Deserialize)]
struct ProviderListResponse {
    providers: Vec<ProviderInfo>,
}

#[derive(Debug, Deserialize)]
struct HealthResponse {
    status: String,
}

fn merge_params<const N: usize>(base: Value, entries: [(String, Option<Value>); N]) -> Value {
    let mut params = match base {
        Value::Object(map) => map,
        _ => Map::new(),
    };

    for (key, value) in entries {
        if let Some(value) = value {
            params.insert(key, value);
        }
    }

    if params.is_empty() {
        Value::Null
    } else {
        Value::Object(params)
    }
}

fn option_entry<T>(key: &str, value: Option<T>) -> (String, Option<Value>)
where
    T: Into<Value>,
{
    (key.to_string(), value.map(Into::into))
}

fn bool_entry(key: &str, value: Option<bool>) -> (String, Option<Value>) {
    (
        key.to_string(),
        value.and_then(|enabled| enabled.then_some(Value::Bool(true))),
    )
}

fn push_query_param(path: &mut String, has_query: &mut bool, key: &str, value: &str) {
    path.push(if *has_query { '&' } else { '?' });
    *has_query = true;
    path.push_str(key);
    path.push('=');
    path.push_str(&percent_encode(value));
}

fn percent_encode(value: &str) -> String {
    const HEX: &[u8; 16] = b"0123456789ABCDEF";
    let mut encoded = String::with_capacity(value.len());

    for byte in value.bytes() {
        if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
            encoded.push(byte as char);
        } else {
            encoded.push('%');
            encoded.push(HEX[(byte >> 4) as usize] as char);
            encoded.push(HEX[(byte & 0x0f) as usize] as char);
        }
    }

    encoded
}
