use std::{collections::HashMap, error::Error, fmt};

use reqwest::{header::HeaderMap, StatusCode};
use serde_json::Value;

use crate::parser::context;

#[derive(Debug, Clone)]
pub struct ResponseCapture {
    pub source: CaptureSource,
    pub target: CaptureTarget,
    pub variable: String,
    pub default: Option<String>,
    pub raw_path: String,
}

#[derive(Debug, Clone)]
pub enum CaptureSource {
    Current,
    Dependency(String),
}

#[derive(Debug, Clone)]
pub enum CaptureTarget {
    Body(Vec<BodyAccessor>),
    Header(HeaderAccessor),
    Status(StatusAccessor),
}

#[derive(Debug, Clone)]
pub enum BodyAccessor {
    Key(String),
    Index(usize),
}

#[derive(Debug, Clone)]
pub struct HeaderAccessor {
    pub name: String,
    pub case_sensitive: bool,
}

#[derive(Debug, Clone)]
pub enum StatusAccessor {
    Code,
    Text,
    Family,
}

#[derive(Debug)]
pub struct CaptureError(pub String);

impl fmt::Display for CaptureError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl Error for CaptureError {}

impl ResponseCapture {
    pub fn parse(raw: &str, context: &HashMap<String, String>) -> Result<Self, CaptureError> {
        let trimmed = raw.trim();
        if !trimmed.starts_with('&') {
            return Err(CaptureError("capture must begin with '&'".into()));
        }

        let remainder = trimmed.trim_start_matches('&').trim_start();
        let (source, remainder) = parse_capture_source(remainder)?;
        let (path_part_raw, remainder_raw) = remainder
            .split_once("->")
            .ok_or_else(|| CaptureError("missing capture assignment '->'".into()))?;
        let path_part = path_part_raw.trim();
        if path_part.is_empty() {
            return Err(CaptureError("missing capture path".into()));
        }
        let remainder = remainder_raw.trim();

        let (variable_part, default_part) = match remainder.split_once(":=") {
            Some((lhs, rhs)) => (lhs.trim(), Some(rhs.trim())),
            None => (remainder, None),
        };

        let variable_part = variable_part.trim();
        if !variable_part.starts_with('$') {
            return Err(CaptureError(
                "capture assignment must target a $variable".into(),
            ));
        }

        let variable = variable_part.trim_start_matches('$').trim().to_string();
        if variable.is_empty() {
            return Err(CaptureError("capture variable name cannot be empty".into()));
        }

        let default = default_part.map(|d| context::inject_from_variable(d, context));

        let target = CaptureTarget::parse(path_part)?;

        Ok(ResponseCapture {
            source,
            target,
            variable,
            default,
            raw_path: path_part.to_string(),
        })
    }

    pub fn extract_from_snapshot(
        &self,
        snapshot: &ResponseSnapshot,
    ) -> Result<Option<(String, String)>, CaptureError> {
        let value = match resolve_capture_value(&self.target, &self.raw_path, snapshot) {
            Ok(val) => Some(val),
            Err(err) => match &self.default {
                Some(default) => Some(default.clone()),
                None => {
                    return Err(CaptureError(format!(
                        "{}",
                        err.0
                            .replace("assertion", &format!("capture into ${}", self.variable))
                    )));
                }
            },
        };

        match value {
            Some(val) => Ok(Some((self.variable.clone(), val))),
            None => Ok(None),
        }
    }

    pub fn required_dependency(&self) -> Option<&str> {
        match &self.source {
            CaptureSource::Dependency(name) => Some(name.as_str()),
            CaptureSource::Current => None,
        }
    }
}

#[derive(Debug, Clone)]
pub struct ResponseSnapshot {
    pub status: StatusCode,
    pub headers: HeaderMap,
    pub body: String,
    pub json: Option<Value>,
}

impl Default for ResponseSnapshot {
    fn default() -> Self {
        Self {
            status: StatusCode::OK,
            headers: HeaderMap::new(),
            body: String::new(),
            json: None,
        }
    }
}

impl CaptureTarget {
    pub fn parse(input: &str) -> Result<Self, CaptureError> {
        let mut chars = input.chars().peekable();
        let mut buffer = String::new();

        while let Some(ch) = chars.peek() {
            if ch.is_whitespace() {
                chars.next();
            } else {
                break;
            }
        }

        while let Some(ch) = chars.peek() {
            if *ch == '.' || ch.is_whitespace() {
                break;
            }
            buffer.push(*ch);
            chars.next();
        }

        let head = buffer.trim();
        let remaining: String = chars.collect();
        let remaining = remaining.trim();

        match head {
            "body" => {
                let segments = parse_body_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Body(segments))
            }
            "header" => {
                let accessor = parse_header_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Header(accessor))
            }
            "status" => {
                let accessor = parse_status_path(remaining.trim_start_matches('.'))?;
                Ok(CaptureTarget::Status(accessor))
            }
            other => Err(CaptureError(format!(
                "unsupported capture target '{}'. expected body, header, or status",
                other
            ))),
        }
    }
}

pub fn parse_capture_operand(
    raw: &str,
) -> Result<(CaptureSource, CaptureTarget, String), CaptureError> {
    let trimmed = raw.trim();
    if !trimmed.starts_with('&') {
        return Err(CaptureError("capture must begin with '&'".into()));
    }

    let remainder = trimmed.trim_start_matches('&').trim_start();
    let (source, remainder) = parse_capture_source(remainder)?;
    let path = remainder.trim();
    if path.is_empty() {
        return Err(CaptureError(
            "capture operand requires a target path".into(),
        ));
    }

    let target = CaptureTarget::parse(path)?;
    Ok((source, target, path.to_string()))
}

pub fn resolve_capture_value(
    target: &CaptureTarget,
    raw_path: &str,
    snapshot: &ResponseSnapshot,
) -> Result<String, CaptureError> {
    match target {
        CaptureTarget::Body(segments) => {
            if segments.is_empty() {
                Ok(snapshot.body.clone())
            } else if let Some(json) = snapshot.json.as_ref() {
                extract_json_value(json, segments).ok_or_else(|| {
                    CaptureError(format!(
                        "Failed to resolve body path '{}': path not found",
                        raw_path
                    ))
                })
            } else {
                Err(CaptureError(format!(
                    "Failed to resolve body path '{}': response body is not valid JSON",
                    raw_path
                )))
            }
        }
        CaptureTarget::Header(accessor) => extract_header_value(&snapshot.headers, accessor)
            .map(|value| value.to_string())
            .ok_or_else(|| {
                CaptureError(format!(
                    "Failed to resolve header '{}' in assertion",
                    accessor.name
                ))
            }),
        CaptureTarget::Status(accessor) => {
            let status_str = match accessor {
                StatusAccessor::Code => snapshot.status.as_u16().to_string(),
                StatusAccessor::Text => {
                    snapshot.status.canonical_reason().unwrap_or("").to_string()
                }
                StatusAccessor::Family => format!("{}xx", snapshot.status.as_u16() / 100),
            };
            Ok(status_str)
        }
    }
}

fn parse_body_path(segments: &str) -> Result<Vec<BodyAccessor>, CaptureError> {
    if segments.is_empty() {
        return Ok(vec![]);
    }

    let raw_segments = split_path_segments(segments)?;
    let mut accessors: Vec<BodyAccessor> = Vec::new();

    for seg in raw_segments {
        if seg.is_empty() {
            continue;
        }

        let mut chars = seg.chars().peekable();
        let mut name = String::new();

        while let Some(ch) = chars.peek() {
            if *ch == '[' {
                break;
            }
            name.push(*ch);
            chars.next();
        }

        let trimmed_name = name.trim();
        if !trimmed_name.is_empty() {
            accessors.push(BodyAccessor::Key(trimmed_name.to_string()));
        }

        while let Some(ch) = chars.next() {
            if ch == '[' {
                let mut index_str = String::new();
                let mut found_closing = false;
                while let Some(inner) = chars.next() {
                    if inner == ']' {
                        found_closing = true;
                        break;
                    }
                    index_str.push(inner);
                }

                if !found_closing {
                    return Err(CaptureError("unmatched '[' in body path".into()));
                }

                if index_str.trim().is_empty() {
                    return Err(CaptureError("array index cannot be empty".into()));
                }

                let index = index_str
                    .trim()
                    .parse::<usize>()
                    .map_err(|_| CaptureError("array index must be a number".into()))?;
                accessors.push(BodyAccessor::Index(index));
            } else if !ch.is_whitespace() {
                return Err(CaptureError(
                    "unexpected character in body path segment".into(),
                ));
            }
        }
    }

    Ok(accessors)
}

fn parse_header_path(segment: &str) -> Result<HeaderAccessor, CaptureError> {
    let raw_segments = split_path_segments(segment)?;
    let name_segment = raw_segments
        .first()
        .ok_or_else(|| CaptureError("header capture requires a header name".into()))?;

    let trimmed = name_segment.trim();
    if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
        let name = trimmed[1..trimmed.len() - 1].to_string();
        Ok(HeaderAccessor {
            name,
            case_sensitive: true,
        })
    } else {
        Ok(HeaderAccessor {
            name: trimmed.to_string(),
            case_sensitive: false,
        })
    }
}

fn parse_status_path(segment: &str) -> Result<StatusAccessor, CaptureError> {
    let raw_segments = split_path_segments(segment)?;
    let field = raw_segments
        .first()
        .map(|s| s.trim().to_string())
        .unwrap_or_default();

    if field.is_empty() {
        return Ok(StatusAccessor::Code);
    }

    match field.as_str() {
        "code" => Ok(StatusAccessor::Code),
        "text" => Ok(StatusAccessor::Text),
        "family" => Ok(StatusAccessor::Family),
        other => Err(CaptureError(format!(
            "unsupported status field '{}'. expected code, text, or family",
            other
        ))),
    }
}

fn split_path_segments(input: &str) -> Result<Vec<String>, CaptureError> {
    if input.is_empty() {
        return Ok(vec![]);
    }

    let mut segments = Vec::new();
    let mut current = String::new();
    let mut chars = input.chars().peekable();
    let mut in_quotes = false;

    while let Some(ch) = chars.next() {
        match ch {
            '"' => {
                current.push(ch);
                in_quotes = !in_quotes;
            }
            '.' if !in_quotes => {
                segments.push(current.trim().to_string());
                current.clear();
            }
            _ => current.push(ch),
        }
    }

    if in_quotes {
        return Err(CaptureError("unterminated quoted segment".into()));
    }

    if !current.trim().is_empty() {
        segments.push(current.trim().to_string());
    }

    Ok(segments)
}

fn extract_json_value(json: &Value, segments: &[BodyAccessor]) -> Option<String> {
    let mut current = json;

    for segment in segments {
        match (segment, current) {
            (&BodyAccessor::Key(ref key), Value::Object(map)) => {
                current = map.get(key)?;
            }
            (&BodyAccessor::Index(index), Value::Array(list)) => {
                current = list.get(index)?;
            }
            _ => return None,
        }
    }

    match current {
        Value::Null => None,
        Value::Bool(b) => Some(b.to_string()),
        Value::Number(num) => Some(num.to_string()),
        Value::String(s) => Some(s.clone()),
        _ => Some(current.to_string()),
    }
}

fn extract_header_value<'a>(headers: &'a HeaderMap, accessor: &HeaderAccessor) -> Option<&'a str> {
    if accessor.case_sensitive {
        if let Some(value) = headers.iter().find_map(|(name, value)| {
            if name.as_str() == accessor.name {
                value.to_str().ok()
            } else {
                None
            }
        }) {
            return Some(value);
        }
    }

    headers.iter().find_map(|(name, value)| {
        if name.as_str().eq_ignore_ascii_case(&accessor.name) {
            value.to_str().ok()
        } else {
            None
        }
    })
}

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

    #[test]
    fn parses_body_capture_and_extracts_value() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& body.token -> $TOKEN", &context).unwrap();

        match &capture.target {
            CaptureTarget::Body(segments) => {
                assert_eq!(segments.len(), 1);
            }
            _ => panic!("expected body capture"),
        }

        let status = StatusCode::OK;
        let headers = HeaderMap::new();
        let body_json = serde_json::json!({ "token": "abc123" });
        let body_text = body_json.to_string();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: Some(body_json.clone()),
        };

        let extracted = capture.extract_from_snapshot(&snapshot).unwrap().unwrap();

        assert_eq!(extracted.0, "TOKEN");
        assert_eq!(extracted.1, "abc123");
    }

    #[test]
    fn extracts_header_case_sensitive() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& header.\"Content-Type\" -> $CT", &context).unwrap();

        let mut headers = HeaderMap::new();
        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
        let status = StatusCode::OK;
        let body_text = String::from("{}");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: body_json.clone(),
        };

        let extracted = capture.extract_from_snapshot(&snapshot).unwrap().unwrap();

        assert_eq!(extracted.0, "CT");
        assert_eq!(extracted.1, "application/json");
    }

    #[test]
    fn uses_default_when_body_path_missing() {
        let mut context = HashMap::new();
        context.insert("fallback".into(), "default".into());
        let capture =
            ResponseCapture::parse("& body.token -> $TOKEN := {{ fallback }}", &context).unwrap();

        let status = StatusCode::OK;
        let headers = HeaderMap::new();
        let body_text = String::from("{}");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: body_json.clone(),
        };

        let extracted = capture.extract_from_snapshot(&snapshot).unwrap().unwrap();

        assert_eq!(extracted.0, "TOKEN");
        assert_eq!(extracted.1, "default");
    }

    #[test]
    fn errors_when_body_path_missing_without_default() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& body.token -> $TOKEN", &context).unwrap();

        let status = StatusCode::OK;
        let headers = HeaderMap::new();
        let body_text = String::from("{}");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text.clone(),
            json: body_json.clone(),
        };

        let result = capture.extract_from_snapshot(&snapshot);

        assert!(result.is_err());
    }

    #[test]
    fn status_capture_defaults_to_code() {
        let context = HashMap::new();
        let capture = ResponseCapture::parse("& status -> $STATUS_CODE", &context).unwrap();

        match capture.target {
            CaptureTarget::Status(StatusAccessor::Code) => {}
            _ => panic!("expected status code accessor"),
        }

        let mut headers = HeaderMap::new();
        headers.insert("content-type", HeaderValue::from_static("text/plain"));
        let status = StatusCode::IM_A_TEAPOT;
        let body_text = String::from("short");
        let body_json = serde_json::from_str::<Value>(&body_text).ok();

        let snapshot = ResponseSnapshot {
            status,
            headers,
            body: body_text,
            json: body_json,
        };

        let extracted = capture.extract_from_snapshot(&snapshot).unwrap().unwrap();
        assert_eq!(extracted.0, "STATUS_CODE");
        assert_eq!(extracted.1, status.as_u16().to_string());
    }
}

fn parse_capture_source(input: &str) -> Result<(CaptureSource, &str), CaptureError> {
    let trimmed = input.trim_start();
    if let Some(rest) = trimmed.strip_prefix('[') {
        let end = rest
            .find(']')
            .ok_or_else(|| CaptureError("unterminated dependency reference".into()))?;
        let name = rest[..end].trim();
        if name.is_empty() {
            return Err(CaptureError("dependency reference cannot be empty".into()));
        }
        let remainder = rest[end + 1..].trim_start();
        let remainder = if remainder.starts_with('.') {
            &remainder[1..]
        } else {
            remainder
        };
        Ok((CaptureSource::Dependency(name.to_string()), remainder))
    } else {
        Ok((CaptureSource::Current, trimmed))
    }
}
