use reqwest::header::{HeaderMap, HeaderValue, ACCEPT};
use reqwest::StatusCode;
use serde_json::Value;
use std::env;
use std::fmt::{Display, Formatter};
use std::time::Duration;
use thiserror::Error;
use urlencoding::encode;
use uuid::Uuid;

const DEFAULT_BASE_URL: &str = "https://api.echoscan.org";
const DEFAULT_TIMEOUT_MS: u64 = 5_000;
const DEFAULT_RETRIES: u32 = 2;
const SDK_USER_AGENT: &str = "echoscan-rust/0.1.0";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
    InvalidRequest,
    AuthFailed,
    Forbidden,
    NotFound,
    QuotaExceeded,
    Timeout,
    UpstreamUnavailable,
    NetworkError,
    UnknownError,
}

impl Display for ErrorCode {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let code = match self {
            ErrorCode::InvalidRequest => "invalid_request",
            ErrorCode::AuthFailed => "auth_failed",
            ErrorCode::Forbidden => "forbidden",
            ErrorCode::NotFound => "not_found",
            ErrorCode::QuotaExceeded => "quota_exceeded",
            ErrorCode::Timeout => "timeout",
            ErrorCode::UpstreamUnavailable => "upstream_unavailable",
            ErrorCode::NetworkError => "network_error",
            ErrorCode::UnknownError => "unknown_error",
        };
        write!(f, "{code}")
    }
}

#[derive(Debug, Error)]
#[error("{message} ({code})")]
pub struct ApiError {
    pub code: ErrorCode,
    pub http_status: u16,
    pub message: String,
    pub request_id: Option<String>,
    pub retryable: bool,
}

#[derive(Debug, Clone)]
pub enum HistoryQuery {
    Days { days: u32, recent: Option<u32> },
    Range {
        from: String,
        to: String,
        recent: Option<u32>,
    },
}

#[derive(Debug, Clone)]
struct RuntimeConfig {
    base_url: String,
    timeout_ms: u64,
    retries: u32,
}

#[derive(Debug, Clone)]
struct BaseClient {
    http: reqwest::Client,
    base_url: String,
    retries: u32,
    api_key: Option<String>,
}

#[derive(Debug, Clone)]
pub struct LiteClient {
    base: BaseClient,
}

#[derive(Debug, Clone)]
pub struct ProClient {
    base: BaseClient,
}

#[derive(Debug, Clone)]
pub struct DebugClient {
    base: BaseClient,
}

impl LiteClient {
    pub fn new() -> Result<Self, ApiError> {
        Ok(Self {
            base: BaseClient::new(None)?,
        })
    }

    pub async fn get_report(&self, imprint: &str) -> Result<Value, ApiError> {
        let imprint = validate_imprint(imprint)?;
        let path = format!("/api/v1/fingerprint/report-lite/{}", encode(&imprint));
        self.base.get_json(&path).await
    }
}

impl ProClient {
    pub fn new(api_key: impl Into<String>) -> Result<Self, ApiError> {
        let api_key = validate_api_key(api_key.into(), "pro")?;
        Ok(Self {
            base: BaseClient::new(Some(api_key))?,
        })
    }

    pub async fn get_report(&self, imprint: &str) -> Result<Value, ApiError> {
        let imprint = validate_imprint(imprint)?;
        let path = format!("/api/v1/fingerprint/report/{}", encode(&imprint));
        self.base.get_json(&path).await
    }

    pub async fn get_history(&self, imprint: &str, query: HistoryQuery) -> Result<Value, ApiError> {
        let imprint = validate_imprint(imprint)?;
        let query_string = build_history_query(query)?;
        let path = format!(
            "/api/v1/fingerprint/imprint/{}/history{}",
            encode(&imprint),
            query_string
        );
        self.base.get_json(&path).await
    }
}

impl DebugClient {
    pub fn new(api_key: impl Into<String>) -> Result<Self, ApiError> {
        let api_key = validate_api_key(api_key.into(), "debug")?;
        Ok(Self {
            base: BaseClient::new(Some(api_key))?,
        })
    }

    pub async fn get_report(&self, imprint: &str) -> Result<Value, ApiError> {
        let imprint = validate_imprint(imprint)?;
        let path = format!("/api/v1/internal/fingerprint/report/{}", encode(&imprint));
        self.base.get_json(&path).await
    }

    pub async fn get_details(&self, imprint: &str) -> Result<Value, ApiError> {
        let imprint = validate_imprint(imprint)?;
        let path = format!("/api/v1/internal/fingerprint/details/{}", encode(&imprint));
        self.base.get_json(&path).await
    }
}

impl BaseClient {
    fn new(api_key: Option<String>) -> Result<Self, ApiError> {
        let runtime = build_runtime_config();
        let http = reqwest::Client::builder()
            .timeout(Duration::from_millis(runtime.timeout_ms))
            .build()
            .map_err(|e| api_error(ErrorCode::UnknownError, 0, format!("Failed to build HTTP client: {e}"), None, false))?;

        Ok(Self {
            http,
            base_url: runtime.base_url,
            retries: runtime.retries,
            api_key,
        })
    }

    async fn get_json(&self, path: &str) -> Result<Value, ApiError> {
        let url = format!("{}{}", self.base_url, path);
        let max_attempts = self.retries + 1;
        let mut last_error: Option<ApiError> = None;

        for attempt in 1..=max_attempts {
            let request_id = format!("req_{}", Uuid::new_v4());
            let mut headers = HeaderMap::new();
            headers.insert(ACCEPT, HeaderValue::from_static("application/json"));
            headers.insert("X-Request-Id", header_value_or_empty(&request_id));
            headers.insert("User-Agent", HeaderValue::from_static(SDK_USER_AGENT));
            if let Some(api_key) = &self.api_key {
                headers.insert("X-API-Key", header_value_or_empty(api_key));
            }

            let request = self.http.get(&url).headers(headers);
            let response = request.send().await;

            match response {
                Ok(res) if res.status().is_success() => {
                    let json = res.json::<Value>().await.unwrap_or(Value::Null);
                    return Ok(json);
                }
                Ok(res) => {
                    let status = res.status();
                    let response_headers = res.headers().clone();
                    let body = res.json::<Value>().await.unwrap_or(Value::Null);
                    let response_request_id = response_headers
                        .get("x-request-id")
                        .and_then(|v| v.to_str().ok())
                        .map(|s| s.to_string())
                        .or_else(|| body.get("requestId").and_then(|v| v.as_str()).map(|s| s.to_string()))
                        .or_else(|| body.get("request_id").and_then(|v| v.as_str()).map(|s| s.to_string()))
                        .or(Some(request_id.clone()));

                    let code = map_status_code(status);
                    let retryable = attempt < max_attempts && (status.is_server_error() || status == StatusCode::TOO_MANY_REQUESTS);
                    let err = api_error(
                        code,
                        status.as_u16(),
                        error_message(code),
                        response_request_id,
                        retryable,
                    );

                    if retryable {
                        last_error = Some(err);
                        continue;
                    }
                    return Err(err);
                }
                Err(err) => {
                    let timeout = err.is_timeout();
                    let code = if timeout {
                        ErrorCode::Timeout
                    } else {
                        ErrorCode::NetworkError
                    };
                    let retryable = attempt < max_attempts;
                    let mapped = api_error(
                        code,
                        if timeout { 408 } else { 0 },
                        error_message(code),
                        Some(request_id),
                        retryable,
                    );
                    if retryable {
                        last_error = Some(mapped);
                        continue;
                    }
                    return Err(mapped);
                }
            }
        }

        Err(last_error.unwrap_or_else(|| {
            api_error(
                ErrorCode::UnknownError,
                0,
                error_message(ErrorCode::UnknownError),
                None,
                false,
            )
        }))
    }
}

fn build_runtime_config() -> RuntimeConfig {
    RuntimeConfig {
        base_url: normalize_base_url(env::var("ECHOSCAN_SERVER_BASE_URL").ok()),
        timeout_ms: parse_u64_env("ECHOSCAN_SERVER_TIMEOUT_MS", DEFAULT_TIMEOUT_MS),
        retries: parse_u32_env("ECHOSCAN_SERVER_RETRIES", DEFAULT_RETRIES),
    }
}

fn normalize_base_url(raw: Option<String>) -> String {
    let mut base = raw
        .map(|v| v.trim().to_string())
        .filter(|v| !v.is_empty())
        .unwrap_or_else(|| DEFAULT_BASE_URL.to_string());
    while base.ends_with('/') {
        base.pop();
    }
    base
}

fn parse_u64_env(name: &str, fallback: u64) -> u64 {
    env::var(name)
        .ok()
        .and_then(|v| v.parse::<u64>().ok())
        .filter(|v| *v > 0)
        .unwrap_or(fallback)
}

fn parse_u32_env(name: &str, fallback: u32) -> u32 {
    env::var(name)
        .ok()
        .and_then(|v| v.parse::<u32>().ok())
        .unwrap_or(fallback)
}

fn validate_api_key(input: String, role: &str) -> Result<String, ApiError> {
    let api_key = input.trim().to_string();
    if api_key.is_empty() {
        return Err(api_error(
            ErrorCode::InvalidRequest,
            400,
            format!("apiKey is required for {role} client"),
            None,
            false,
        ));
    }
    Ok(api_key)
}

fn validate_imprint(input: &str) -> Result<String, ApiError> {
    let imprint = input.trim().to_string();
    if imprint.is_empty() {
        return Err(api_error(
            ErrorCode::InvalidRequest,
            400,
            "imprint must be a non-empty string".to_string(),
            None,
            false,
        ));
    }
    Ok(imprint)
}

fn build_history_query(query: HistoryQuery) -> Result<String, ApiError> {
    let params = match query {
        HistoryQuery::Days { days, recent } => {
            if days == 0 {
                return Err(api_error(
                    ErrorCode::InvalidRequest,
                    400,
                    "history query: days must be a positive number".to_string(),
                    None,
                    false,
                ));
            }

            let mut pairs = vec![format!("days={days}")];
            if let Some(recent) = recent {
                if recent == 0 {
                    return Err(api_error(
                        ErrorCode::InvalidRequest,
                        400,
                        "history query: recent must be a positive number".to_string(),
                        None,
                        false,
                    ));
                }
                pairs.push(format!("recent={recent}"));
            }
            pairs
        }
        HistoryQuery::Range { from, to, recent } => {
            validate_yyyy_mm_dd(&from, "from")?;
            validate_yyyy_mm_dd(&to, "to")?;
            if from > to {
                return Err(api_error(
                    ErrorCode::InvalidRequest,
                    400,
                    "history query: from must be <= to".to_string(),
                    None,
                    false,
                ));
            }

            let mut pairs = vec![format!("from={from}"), format!("to={to}")];
            if let Some(recent) = recent {
                if recent == 0 {
                    return Err(api_error(
                        ErrorCode::InvalidRequest,
                        400,
                        "history query: recent must be a positive number".to_string(),
                        None,
                        false,
                    ));
                }
                pairs.push(format!("recent={recent}"));
            }
            pairs
        }
    };

    Ok(format!("?{}", params.join("&")))
}

fn validate_yyyy_mm_dd(date: &str, field: &str) -> Result<(), ApiError> {
    let bytes = date.as_bytes();
    let valid = bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(idx, b)| matches!(idx, 4 | 7) || b.is_ascii_digit());
    if !valid {
        return Err(api_error(
            ErrorCode::InvalidRequest,
            400,
            format!("{field} must use YYYY-MM-DD format"),
            None,
            false,
        ));
    }
    Ok(())
}

fn map_status_code(status: StatusCode) -> ErrorCode {
    match status.as_u16() {
        400 => ErrorCode::InvalidRequest,
        401 => ErrorCode::AuthFailed,
        403 => ErrorCode::Forbidden,
        404 => ErrorCode::NotFound,
        408 => ErrorCode::Timeout,
        429 => ErrorCode::QuotaExceeded,
        500 | 502 | 503 | 504 => ErrorCode::UpstreamUnavailable,
        _ => ErrorCode::UnknownError,
    }
}

fn error_message(code: ErrorCode) -> String {
    match code {
        ErrorCode::InvalidRequest => "Invalid request",
        ErrorCode::AuthFailed => "Authentication failed",
        ErrorCode::Forbidden => "Forbidden",
        ErrorCode::NotFound => "Not found",
        ErrorCode::QuotaExceeded => "Quota exceeded",
        ErrorCode::Timeout => "Request timed out",
        ErrorCode::UpstreamUnavailable => "Upstream service unavailable",
        ErrorCode::NetworkError => "Network error",
        ErrorCode::UnknownError => "Unexpected error",
    }
    .to_string()
}

fn api_error(
    code: ErrorCode,
    http_status: u16,
    message: String,
    request_id: Option<String>,
    retryable: bool,
) -> ApiError {
    ApiError {
        code,
        http_status,
        message,
        request_id,
        retryable,
    }
}

fn header_value_or_empty(value: &str) -> HeaderValue {
    HeaderValue::from_str(value).unwrap_or_else(|_| HeaderValue::from_static(""))
}
