use anyhow::{Context, Result};
use reqwest::StatusCode;
use reqwest::blocking::Client;
use serde_json::Value;
use thiserror::Error;

const MAIL_KEY_NAMES: &[&str] = &["mail", "mails"];

#[derive(Debug, Clone)]
pub struct MailItem {
    pub id: String,
    pub item_type: MailType,
    pub title: String,
    pub status: Option<String>,
    pub timestamp: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MailType {
    Letter,
    Package,
    Other(String),
}

impl MailType {
    pub fn from_api(value: &str) -> Self {
        match value.trim().to_ascii_lowercase().as_str() {
            "letter" => Self::Letter,
            "package" => Self::Package,
            other => Self::Other(other.to_string()),
        }
    }

    pub fn label(&self) -> String {
        match self {
            Self::Letter => "Letter".to_string(),
            Self::Package => "Package".to_string(),
            Self::Other(other) => title_case(other),
        }
    }

    pub fn detail_path(&self, id: &str) -> Option<String> {
        match self {
            Self::Letter => Some(format!("/api/public/v1/letters/{id}")),
            Self::Package => Some(format!("/api/public/v1/packages/{id}")),
            Self::Other(_) => None,
        }
    }

    pub fn detail_root_key(&self) -> Option<&'static str> {
        match self {
            Self::Letter => Some("letter"),
            Self::Package => Some("package"),
            Self::Other(_) => None,
        }
    }
}

#[derive(Debug, Error)]
pub enum MailError {
    #[error("your Hack Club Mail API key was rejected")]
    InvalidAuth,
    #[error("the mail API returned {status}: {message}")]
    Api { status: StatusCode, message: String },
    #[error("network error: {0}")]
    Request(#[from] reqwest::Error),
    #[error("failed to decode JSON response: {0}")]
    Json(#[from] serde_json::Error),
}

pub struct ApiClient {
    http: Client,
    base_url: String,
}

impl ApiClient {
    pub fn new(base_url: &str) -> Result<Self> {
        let http = Client::builder()
            .user_agent(format!("hc-mail/{}", env!("CARGO_PKG_VERSION")))
            .build()
            .context("failed to build HTTP client")?;

        Ok(Self {
            http,
            base_url: base_url.trim_end_matches('/').to_string(),
        })
    }

    pub fn fetch_me(&self, api_key: &str) -> Result<Value, MailError> {
        self.get_json("/api/public/v1/me", api_key)
    }

    pub fn validate_api_key(&self, api_key: &str) -> Result<(), MailError> {
        self.fetch_me(api_key).map(|_| ())
    }

    pub fn fetch_mail(&self, api_key: &str) -> Result<Vec<MailItem>, MailError> {
        let payload = self.get_json("/api/public/v1/mail", api_key)?;
        let entries = MAIL_KEY_NAMES
            .iter()
            .find_map(|key| payload.get(*key).and_then(Value::as_array))
            .ok_or_else(|| MailError::Api {
                status: StatusCode::OK,
                message: "mail list response did not include a `mail` array".to_string(),
            })?;

        Ok(entries.iter().filter_map(parse_mail_item).collect())
    }

    pub fn fetch_detail(&self, api_key: &str, item: &MailItem) -> Result<Value, MailError> {
        let path = item.detail_path().ok_or_else(|| MailError::Api {
            status: StatusCode::NOT_IMPLEMENTED,
            message: format!(
                "mail type `{}` is not supported yet",
                item.item_type.label()
            ),
        })?;

        self.get_json(&path, api_key)
    }

    fn get_json(&self, path: &str, api_key: &str) -> Result<Value, MailError> {
        let url = format!("{}{}", self.base_url, path);
        let response = self.http.get(url).bearer_auth(api_key).send()?;
        let status = response.status();
        let body = response.text()?;

        if !status.is_success() {
            let message = extract_error_message(&body)
                .unwrap_or_else(|| format!("request failed with status {status}"));

            if status == StatusCode::UNAUTHORIZED && message == "invalid_auth" {
                return Err(MailError::InvalidAuth);
            }

            return Err(MailError::Api { status, message });
        }

        Ok(serde_json::from_str(&body)?)
    }
}

impl MailItem {
    fn detail_path(&self) -> Option<String> {
        self.item_type.detail_path(&self.id)
    }
}

fn parse_mail_item(value: &Value) -> Option<MailItem> {
    let id = lookup_string(value, &["id"])?;
    let kind =
        lookup_string(value, &["type", "mail_type", "kind"]).unwrap_or_else(|| "mail".to_string());
    let item_type = MailType::from_api(&kind);
    let title = lookup_string(value, &["title", "name", "subject"])
        .unwrap_or_else(|| format!("{} {}", item_type.label(), id));
    let status = lookup_string(value, &["status", "state"]);
    let timestamp = lookup_string(
        value,
        &[
            "created_at",
            "received_at",
            "updated_at",
            "sent_at",
            "happened_at",
        ],
    );

    Some(MailItem {
        id,
        item_type,
        title,
        status,
        timestamp,
    })
}

fn lookup_string(value: &Value, keys: &[&str]) -> Option<String> {
    let object = value.as_object()?;

    for key in keys {
        if let Some(entry) = object.get(*key) {
            match entry {
                Value::String(text) => {
                    let trimmed = text.trim();
                    if !trimmed.is_empty() {
                        return Some(trimmed.to_string());
                    }
                }
                Value::Number(number) => return Some(number.to_string()),
                Value::Bool(boolean) => return Some(boolean.to_string()),
                _ => {}
            }
        }
    }

    None
}

fn extract_error_message(body: &str) -> Option<String> {
    let payload = serde_json::from_str::<Value>(body).ok()?;

    for key in ["error", "message"] {
        if let Some(value) = payload.get(key).and_then(Value::as_str) {
            let trimmed = value.trim();
            if !trimmed.is_empty() {
                return Some(trimmed.to_string());
            }
        }
    }

    None
}

fn title_case(value: &str) -> String {
    value
        .split(['_', '-', ' '])
        .filter(|part| !part.is_empty())
        .map(|part| {
            let mut chars = part.chars();
            match chars.next() {
                Some(first) => format!("{}{}", first.to_ascii_uppercase(), chars.as_str()),
                None => String::new(),
            }
        })
        .collect::<Vec<_>>()
        .join(" ")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_mail_items_from_public_mail_payload() {
        let payload = serde_json::json!({
            "mail": [
                { "id": "abc", "type": "letter", "title": "Hello world" },
                { "id": "def", "type": "package", "title": "Sticker pack", "status": "delivered" }
            ]
        });

        let items = payload["mail"]
            .as_array()
            .unwrap()
            .iter()
            .filter_map(parse_mail_item)
            .collect::<Vec<_>>();

        assert_eq!(items.len(), 2);
        assert_eq!(items[0].id, "abc");
        assert_eq!(items[0].item_type, MailType::Letter);
        assert_eq!(items[1].status.as_deref(), Some("delivered"));
    }
}
// isdjfiosdjfosdfjosdifj
