use std::sync::{Mutex, OnceLock};
use std::thread;
use std::time::{Duration, Instant};

use serde::Serialize;
use serde_json::{Map, Value};

use crate::MacosError;

const REDDIT_BASE: &str = "https://old.reddit.com";
const REDDIT_OPERATION_DELAY: Duration = Duration::from_secs(1);
const REDDIT_MAX_RETRIES: usize = 3;
const REDDIT_UA: &str = concat!(
    "cueward/",
    env!("CARGO_PKG_VERSION"),
    " (+https://github.com/Termdock-dev/cueward)"
);

mod scan;
#[cfg(test)]
mod tests;

pub use scan::{feed, post, search};

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct RedditSubredditInfo {
    pub name: String,
    pub display_name: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub title: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subscribers: Option<u64>,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct RedditPostSummary {
    pub id: String,
    pub title: String,
    pub author: String,
    pub subreddit: String,
    pub url: String,
    pub permalink: String,
    pub score: i64,
    pub num_comments: u64,
    pub created_utc: i64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub selftext: Option<String>,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct RedditComment {
    pub id: String,
    pub author: String,
    pub body: String,
    pub score: i64,
    pub created_utc: i64,
    pub permalink: String,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct RedditFeedResult {
    pub subreddit: RedditSubredditInfo,
    pub posts: Vec<RedditPostSummary>,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct RedditPostResult {
    pub post: RedditPostSummary,
    pub comments: Vec<RedditComment>,
}

#[derive(Debug, Serialize, PartialEq, Eq)]
pub struct RedditSearchResult {
    pub query: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub subreddit: Option<String>,
    pub limit: usize,
    pub posts: Vec<RedditPostSummary>,
}

pub(super) fn validate_limit(limit: usize) -> Result<(), MacosError> {
    if limit == 0 {
        return Err(MacosError::Other("reddit limit must be greater than 0".to_string()));
    }
    Ok(())
}

pub(super) fn normalize_subreddit(input: &str) -> Result<String, MacosError> {
    let trimmed = input.trim();
    let trimmed = trimmed.strip_prefix("r/").unwrap_or(trimmed);
    let trimmed = trimmed.strip_prefix("R/").unwrap_or(trimmed);
    let trimmed = trimmed.trim_matches('/');
    if trimmed.is_empty() || trimmed.contains('/') {
        return Err(MacosError::Other(format!("invalid subreddit: {input}")));
    }
    Ok(trimmed.to_ascii_lowercase())
}

pub(super) fn normalize_post_url(input: &str) -> Result<String, MacosError> {
    let trimmed = input.trim();
    let without_scheme = trimmed
        .strip_prefix("https://")
        .or_else(|| trimmed.strip_prefix("http://"))
        .unwrap_or(trimmed);
    let (host, rest) = without_scheme
        .split_once('/')
        .ok_or_else(|| MacosError::Other(format!("invalid reddit post url: {input}")))?;
    let host = host.to_ascii_lowercase();
    if !matches!(host.as_str(), "reddit.com" | "www.reddit.com" | "old.reddit.com") {
        return Err(MacosError::Other(format!("invalid reddit post url: {input}")));
    }
    let path_only = rest
        .split('?')
        .next()
        .unwrap_or(rest)
        .split('#')
        .next()
        .unwrap_or(rest);
    let path = path_only.trim_matches('/');
    let segments: Vec<&str> = path.split('/').collect();
    if segments.len() < 4 || segments[0] != "r" || segments[2] != "comments" {
        return Err(MacosError::Other(format!("invalid reddit post url: {input}")));
    }
    let subreddit = normalize_subreddit(segments[1])?;
    let post_id = segments[3];
    if post_id.is_empty() {
        return Err(MacosError::Other(format!("invalid reddit post url: {input}")));
    }
    let slug = segments.get(4).copied().unwrap_or("");
    let suffix = if slug.is_empty() {
        String::new()
    } else {
        format!("/{slug}")
    };
    Ok(format!(
        "{REDDIT_BASE}/r/{subreddit}/comments/{post_id}{suffix}.json?limit=500"
    ))
}

pub(super) fn build_about_url(subreddit: &str) -> String {
    format!("{REDDIT_BASE}/r/{subreddit}/about.json")
}

pub(super) fn build_feed_url(subreddit: &str, limit: usize) -> String {
    format!("{REDDIT_BASE}/r/{subreddit}.json?limit={limit}")
}

pub(super) fn build_search_url(query: &str, subreddit: Option<&str>, limit: usize) -> String {
    let query = urlencoding::encode(query);
    match subreddit {
        Some(subreddit) => format!(
            "{REDDIT_BASE}/r/{subreddit}/search.json?q={query}&restrict_sr=on&limit={limit}&sort=relevance"
        ),
        None => format!("{REDDIT_BASE}/search.json?q={query}&limit={limit}&sort=relevance"),
    }
}

pub(super) enum FetchJsonError {
    NotFound,
    Other(MacosError),
}

impl From<FetchJsonError> for MacosError {
    fn from(value: FetchJsonError) -> Self {
        match value {
            FetchJsonError::NotFound => {
                MacosError::Other("reddit target not found".to_string())
            }
            FetchJsonError::Other(error) => error,
        }
    }
}

pub(super) fn fetch_json(url: &str) -> Result<Value, FetchJsonError> {
    let mut last_error = None;

    for attempt in 0..=REDDIT_MAX_RETRIES {
        throttle_reddit_request().map_err(FetchJsonError::Other)?;
        let response = ureq::get(url)
            .set("User-Agent", REDDIT_UA)
            .set("Accept", "application/json")
            .call();
        match response {
            Ok(response) => {
                let body = response.into_string().map_err(|error| {
                    FetchJsonError::Other(MacosError::Other(format!(
                        "failed to read reddit response: {error}"
                    )))
                })?;
                return serde_json::from_str(&body).map_err(|error| {
                    FetchJsonError::Other(MacosError::Other(format!(
                        "invalid reddit json payload: {error}"
                    )))
                });
            }
            Err(ureq::Error::Status(status, response)) => {
                let retry_after = retry_after_delay(&response);
                let body = response.into_string().unwrap_or_default();
                if status == 404 {
                    return Err(FetchJsonError::NotFound);
                }
                if should_retry_status(status) && attempt < REDDIT_MAX_RETRIES {
                    last_error = Some(format!(
                        "reddit returned retryable status {status}: {}",
                        body.trim()
                    ));
                    thread::sleep(retry_after.unwrap_or_else(|| reddit_backoff(attempt)));
                    continue;
                }
                return Err(FetchJsonError::Other(MacosError::Other(format!(
                    "reddit returned unexpected status {status}: {}",
                    body.trim()
                ))));
            }
            Err(ureq::Error::Transport(error)) => {
                if attempt < REDDIT_MAX_RETRIES {
                    last_error = Some(format!("reddit request failed: {error}"));
                    thread::sleep(reddit_backoff(attempt));
                    continue;
                }
                return Err(FetchJsonError::Other(MacosError::Other(format!(
                    "reddit request failed: {error}"
                ))));
            }
        }
    }

    Err(FetchJsonError::Other(MacosError::Other(
        last_error.unwrap_or_else(|| "reddit request failed after retries".to_string()),
    )))
}

fn reddit_request_state() -> &'static Mutex<Option<Instant>> {
    static STATE: OnceLock<Mutex<Option<Instant>>> = OnceLock::new();
    STATE.get_or_init(|| Mutex::new(None))
}

fn compute_next_reddit_request(
    now: Instant,
    last_request_at: Option<Instant>,
) -> (Option<Duration>, Instant) {
    match last_request_at {
        Some(last) if now < last + REDDIT_OPERATION_DELAY => {
            let next_allowed = last + REDDIT_OPERATION_DELAY;
            (Some(next_allowed - now), next_allowed)
        }
        _ => (None, now),
    }
}

fn throttle_reddit_request() -> Result<(), MacosError> {
    let sleep_for = {
        let mut guard = reddit_request_state()
            .lock()
            .map_err(|_| MacosError::Other("reddit request state poisoned".to_string()))?;
        let now = Instant::now();
        let (delay, next_allowed) = compute_next_reddit_request(now, *guard);
        *guard = Some(next_allowed);
        delay
    };

    if let Some(delay) = sleep_for {
        thread::sleep(delay);
    }
    Ok(())
}

fn should_retry_status(status: u16) -> bool {
    status == 429 || (500..600).contains(&status)
}

fn retry_after_delay(response: &ureq::Response) -> Option<Duration> {
    response
        .header("Retry-After")
        .and_then(|value| value.trim().parse::<u64>().ok())
        .map(Duration::from_secs)
}

fn reddit_backoff(attempt: usize) -> Duration {
    Duration::from_secs(2 * (attempt as u64 + 1))
}

pub(super) fn parse_subreddit_info(root: &Value) -> Result<RedditSubredditInfo, MacosError> {
    let data = object(root, "reddit about payload")?
        .get("data")
        .ok_or_else(|| MacosError::Other("invalid reddit json payload: missing about data".to_string()))
        .and_then(|value| object(value, "reddit about data"))?;
    Ok(RedditSubredditInfo {
        name: string(data, "display_name").unwrap_or_default().to_ascii_lowercase(),
        display_name: string(data, "display_name_prefixed").unwrap_or_default(),
        title: optional_string(data, "title"),
        description: optional_string(data, "public_description"),
        subscribers: optional_u64(data, "subscribers"),
    })
}

pub(super) fn parse_listing_posts(root: &Value) -> Result<Vec<RedditPostSummary>, MacosError> {
    let children = listing_children(root)?;
    Ok(children
        .iter()
        .filter_map(|child| thing_data(child).ok())
        .filter_map(parse_post_summary)
        .collect())
}

pub(super) fn parse_post_result(root: &Value) -> Result<RedditPostResult, MacosError> {
    let items = array(root, "reddit post payload")?;
    let post_listing = items
        .first()
        .ok_or_else(|| MacosError::Other("invalid reddit post payload: missing post listing".to_string()))?;
    let comments_listing = items
        .get(1)
        .ok_or_else(|| MacosError::Other("invalid reddit post payload: missing comments listing".to_string()))?;
    let post = listing_children(post_listing)?
        .iter()
        .filter_map(|child| thing_data(child).ok())
        .find_map(parse_post_summary)
        .ok_or_else(|| MacosError::Other("post payload missing article data".to_string()))?;
    let comments = listing_children(comments_listing)?
        .iter()
        .filter(|child| child.get("kind").and_then(Value::as_str) == Some("t1"))
        .filter_map(|child| thing_data(child).ok())
        .filter_map(parse_comment)
        .collect();
    Ok(RedditPostResult { post, comments })
}

fn listing_children(root: &Value) -> Result<&Vec<Value>, MacosError> {
    let data = object(root, "reddit listing payload")?
        .get("data")
        .ok_or_else(|| MacosError::Other("invalid reddit json payload: missing listing data".to_string()))
        .and_then(|value| object(value, "reddit listing data"))?;
    data.get("children")
        .ok_or_else(|| MacosError::Other("invalid reddit json payload: missing listing children".to_string()))
        .and_then(|value| array(value, "reddit listing children"))
}

fn thing_data(root: &Value) -> Result<&Map<String, Value>, MacosError> {
    object(root, "reddit thing")?
        .get("data")
        .ok_or_else(|| MacosError::Other("invalid reddit thing: missing data".to_string()))
        .and_then(|value| object(value, "reddit thing data"))
}

fn parse_post_summary(data: &Map<String, Value>) -> Option<RedditPostSummary> {
    Some(RedditPostSummary {
        id: string(data, "id")?,
        title: string(data, "title")?,
        author: string(data, "author")?,
        subreddit: string(data, "subreddit")?,
        url: string(data, "url")?,
        permalink: prefixed_permalink(optional_string(data, "permalink")?)?,
        score: optional_i64(data, "score").unwrap_or(0),
        num_comments: optional_u64(data, "num_comments").unwrap_or(0),
        created_utc: optional_i64(data, "created_utc").unwrap_or(0),
        selftext: optional_string(data, "selftext").filter(|value| !value.is_empty()),
    })
}

fn parse_comment(data: &Map<String, Value>) -> Option<RedditComment> {
    Some(RedditComment {
        id: string(data, "id")?,
        author: string(data, "author")?,
        body: string(data, "body")?,
        score: optional_i64(data, "score").unwrap_or(0),
        created_utc: optional_i64(data, "created_utc").unwrap_or(0),
        permalink: prefixed_permalink(optional_string(data, "permalink")?)?,
    })
}

fn prefixed_permalink(path: String) -> Option<String> {
    if path.is_empty() {
        return None;
    }
    Some(if path.starts_with("http://") || path.starts_with("https://") {
        path
    } else {
        format!("https://reddit.com{path}")
    })
}

fn object<'a>(value: &'a Value, label: &str) -> Result<&'a Map<String, Value>, MacosError> {
    value
        .as_object()
        .ok_or_else(|| MacosError::Other(format!("invalid {label}: expected object")))
}

fn array<'a>(value: &'a Value, label: &str) -> Result<&'a Vec<Value>, MacosError> {
    value
        .as_array()
        .ok_or_else(|| MacosError::Other(format!("invalid {label}: expected array")))
}

fn string(data: &Map<String, Value>, key: &str) -> Option<String> {
    data.get(key).and_then(Value::as_str).map(ToOwned::to_owned)
}

fn optional_string(data: &Map<String, Value>, key: &str) -> Option<String> {
    string(data, key)
}

fn optional_u64(data: &Map<String, Value>, key: &str) -> Option<u64> {
    data.get(key).and_then(|value| {
        value.as_u64().or_else(|| {
            value
                .as_i64()
                .and_then(|n| if n >= 0 { Some(n as u64) } else { None })
        })
    })
}

fn optional_i64(data: &Map<String, Value>, key: &str) -> Option<i64> {
    data.get(key).and_then(|value| {
        value
            .as_i64()
            .or_else(|| value.as_u64().map(|n| n as i64))
            .or_else(|| value.as_f64().map(|n| n as i64))
    })
}
