use std::sync::Arc;
use std::sync::OnceLock;
use std::time::Duration;

use anyhow::{Context, Result};
use pmoupnp::soap::{build_soap_request, parse_soap_envelope, SoapEnvelope};
use tracing::{debug, trace, warn, info};
use ureq::Agent;

use crate::errors::ControlPointError;

static SOAP_AGENT: OnceLock<Arc<Agent>> = OnceLock::new();

/// Number of retries for transient transport failures on control actions.
const SOAP_CONTROL_MAX_RETRIES: usize = 2;
/// Delay between retries (ms). Kept short: devices usually recover in <200 ms.
const SOAP_CONTROL_RETRY_DELAY_MS: u64 = 300;

fn get_soap_agent() -> Arc<Agent> {
    SOAP_AGENT
        .get_or_init(|| {
            Arc::new(
                Agent::config_builder()
                    .http_status_as_error(false)
                    .timeout_global(Some(Duration::from_secs(30)))
                    .build()
                    .into(),
            )
        })
        .clone()
}

/// Result of a SOAP call:
/// - HTTP status code
/// - raw XML body (always)
/// - parsed SOAP envelope if parsing succeeded
pub struct SoapCallResult {
    pub status: ureq::http::StatusCode,
    pub raw_body: String,
    pub envelope: Option<SoapEnvelope>,
}

pub fn build_soap_body(
    action: &str,
    service_type: &str,
    args: &[(&str, &str)],
) -> Result<String, xmltree::Error> {
    build_soap_request(service_type, action, args)
}

/// Invoke a UPnP SOAP control action with automatic retry on transient failures.
///
/// Use this for **control operations** (Play, Pause, Stop, SetAVTransportURI, …)
/// where a transient TCP error should be absorbed silently.
///
/// Retries are attempted only on [`ControlPointError::is_transient_soap_error`]
/// (i.e. transport-level failures). Protocol-level errors (UPnP faults,
/// HTTP 4xx/5xx with a valid body) propagate immediately without retry.
///
/// For **polling reads** (GetTransportInfo, GetPositionInfo, …) prefer
/// [`invoke_upnp_action_with_timeout`] directly so that a slow device does
/// not hold the watcher thread for multiple retry cycles.
pub fn invoke_upnp_action(
    control_url: &str,
    service_type: &str,
    action: &str,
    args: &[(&str, &str)],
) -> Result<SoapCallResult, ControlPointError> {
    let retry_delay = Duration::from_millis(SOAP_CONTROL_RETRY_DELAY_MS);

    for attempt in 0..=SOAP_CONTROL_MAX_RETRIES {
        match invoke_upnp_action_with_timeout(control_url, service_type, action, args, None) {
            Ok(result) => return Ok(result),
            Err(e) if e.is_transient_soap_error() && attempt < SOAP_CONTROL_MAX_RETRIES => {
                info!(
                    url = control_url,
                    action = action,
                    attempt = attempt + 1,
                    error = %e,
                    "Transient SOAP error, retrying"
                );
                std::thread::sleep(retry_delay);
            }
            Err(e) => return Err(e),
        }
    }
    // Unreachable: the loop either returns Ok or propagates Err above.
    unreachable!()
}

pub fn invoke_upnp_action_with_timeout(
    control_url: &str,
    service_type: &str,
    action: &str,
    args: &[(&str, &str)],
    timeout: Option<Duration>,
) -> Result<SoapCallResult, ControlPointError> {
    let body_xml = build_soap_body(action, service_type, args)
        .context("Failed to build SOAP request body")
        .map_err(|e| ControlPointError::SoapAction(format!("{}", e)))?;

    let arg_log = summarize_args_for_log(args);
    debug!(
        url = control_url,
        action = action,
        service_type = service_type,
        args = ?arg_log,
        "Sending SOAP request"
    );

    trace!(body = body_xml.as_str(), "SOAP request body");

    let request = if let Some(duration) = timeout {
        let config = Agent::config_builder()
            .http_status_as_error(false)
            .timeout_global(Some(duration))
            .build();
        let agent: Agent = config.into();
        agent.post(control_url)
    } else {
        get_soap_agent().post(control_url)
    };

    let soap_action_header = format!(r#""{}#{}""#, service_type, action);

    let mut response = request
        .header("Content-Type", r#"text/xml; charset="utf-8""#)
        .header("SOAPAction", &soap_action_header)
        .send(body_xml)
        .with_context(|| format!("HTTP error when sending SOAP request to {}", control_url))
        .map_err(|e| ControlPointError::SoapAction(format!("{}", e)))?;

    let status = response.status();
    debug!(status = status.as_u16(), "SOAP response received");

    // 5. Read full body
    //
    //    API réelle (ureq 3.1.4):
    //    body_mut().read_to_string() -> Result<String, ureq::Error>
    let raw_body = response
        .body_mut()
        .read_to_string()
        .context("Failed to read SOAP response body")
        .map_err(|e| ControlPointError::SoapAction(format!("{}", e)))?;

    // 6. Try to parse SOAP envelope; non-fatal on failure
    let parsed_envelope = parse_soap_envelope(raw_body.as_bytes()).ok();

    if !status.is_success() {
        if is_oh_info_invalid_action(service_type, action, parsed_envelope.as_ref()) {
            debug!(
                url = control_url,
                action = action,
                service_type = service_type,
                status = status.as_u16(),
                "OpenHome Info action not supported (Invalid Action)"
            );
        } else {
            warn!(
                url = control_url,
                action = action,
                service_type = service_type,
                status = status.as_u16(),
                body_snippet = %response_snippet(&raw_body),
                "SOAP call returned non-success status"
            );
        }
    }

    Ok(SoapCallResult {
        status,
        raw_body,
        envelope: parsed_envelope,
    })
}

fn summarize_args_for_log<'a>(args: &'a [(&'a str, &'a str)]) -> Vec<String> {
    args.iter()
        .map(|(name, value)| format!("{}:{}B {}", name, value.len(), preview_value(value)))
        .collect()
}

fn preview_value(value: &str) -> String {
    const MAX_PREVIEW: usize = 96;
    if value.len() <= MAX_PREVIEW {
        value.to_string()
    } else {
        format!("{}…", &value[..MAX_PREVIEW])
    }
}

fn response_snippet(body: &str) -> String {
    const MAX_LEN: usize = 256;
    let trimmed = body.trim();
    if trimmed.len() <= MAX_LEN {
        trimmed.to_string()
    } else {
        format!("{}…", &trimmed[..MAX_LEN])
    }
}

fn is_oh_info_invalid_action(
    service_type: &str,
    action: &str,
    envelope: Option<&SoapEnvelope>,
) -> bool {
    if service_type != "urn:av-openhome-org:service:Info:1" {
        return false;
    }
    if action != "Id" && action != "TransportState" {
        return false;
    }
    let Some(env) = envelope else {
        return false;
    };
    match parse_upnp_error(env) {
        Some(err) if err.error_code == 401 => true,
        _ => false,
    }
}

#[derive(Debug, Clone)]
pub struct UpnpError {
    pub error_code: u32,
    pub error_description: String,
}

pub fn parse_upnp_error(envelope: &SoapEnvelope) -> Option<UpnpError> {
    let fault = find_child_with_suffix(&envelope.body.content, "Fault")?;
    let detail = find_child_with_suffix(fault, "detail")?;
    let upnp_error = find_child_with_suffix(detail, "UPnPError")?;

    let error_code_elem = upnp_error.children.iter().find_map(|node| match node {
        xmltree::XMLNode::Element(elem) if elem.name.ends_with("errorCode") => Some(elem),
        _ => None,
    })?;

    let error_code_text = error_code_elem.get_text()?.trim().to_string();
    let error_code = error_code_text.parse::<u32>().ok()?;

    let error_description = upnp_error
        .children
        .iter()
        .find_map(|node| match node {
            xmltree::XMLNode::Element(elem) if elem.name.ends_with("errorDescription") => {
                elem.get_text().map(|t| t.trim().to_string())
            }
            _ => None,
        })
        .unwrap_or_default();

    Some(UpnpError {
        error_code,
        error_description,
    })
}

pub fn find_child_with_suffix<'a>(
    parent: &'a xmltree::Element,
    suffix: &str,
) -> Option<&'a xmltree::Element> {
    parent.children.iter().find_map(|node| match node {
        xmltree::XMLNode::Element(elem) if elem.name.ends_with(suffix) => Some(elem),
        _ => None,
    })
}

pub fn find_child_with_local_name<'a>(
    parent: &'a xmltree::Element,
    local: &str,
) -> Option<&'a xmltree::Element> {
    parent.children.iter().find_map(|node| {
        if let xmltree::XMLNode::Element(elem) = node {
            if elem.name == local || elem.name.ends_with(&format!(":{}", local)) {
                return Some(elem);
            }
        }
        None
    })
}

/// Ensures the SOAP call succeeded, returns error if HTTP status or UPnP error occurred
pub fn ensure_success(action: &str, call_result: &SoapCallResult) -> Result<(), ControlPointError> {
    if call_result.status.is_success() {
        if let Some(env) = &call_result.envelope {
            if let Some(err) = parse_upnp_error(env) {
                return Err(ControlPointError::SoapUpnpParseError(
                    action.to_string(),
                    err.error_code,
                    err.error_description,
                    call_result.status.as_u16() as u32,
                ));
            }
        }
        return Ok(());
    }

    if let Some(env) = &call_result.envelope {
        if let Some(err) = parse_upnp_error(env) {
            return Err(ControlPointError::SoapUpnpParseError(
                action.to_string(),
                err.error_code,
                err.error_description,
                call_result.status.as_u16() as u32,
            ));
        }
    }
    Err(ControlPointError::SoapActionWrongBody(
        action.to_string(),
        call_result.status.as_u16() as u32,
        call_result.raw_body.clone(),
    ))
}

/// Ensures success and returns the SOAP envelope (for openhome_client compatibility)
pub fn ensure_success_with_envelope<'a>(
    action: &str,
    call_result: &'a SoapCallResult,
) -> Result<&'a SoapEnvelope, ControlPointError> {
    if !call_result.status.is_success() {
        if let Some(env) = &call_result.envelope {
            if let Some(err) = parse_upnp_error(env) {
                return Err(ControlPointError::SoapUpnpParseError(
                    action.to_string(),
                    err.error_code,
                    err.error_description,
                    call_result.status.as_u16() as u32,
                ));
            }
        }

        return Err(ControlPointError::SoapActionWrongBody(
            action.to_string(),
            call_result.status.as_u16() as u32,
            call_result.raw_body.clone(),
        ));
    }

    let envelope = call_result
        .envelope
        .as_ref()
        .ok_or_else(|| ControlPointError::SoapNoEnvelop(action.to_string()))?;

    if let Some(err) = parse_upnp_error(envelope) {
        return Err(ControlPointError::SoapUpnpParseError(
            action.to_string(),
            err.error_code,
            err.error_description,
            call_result.status.as_u16() as u32,
        ));
    }

    Ok(envelope)
}

/// Handles action response, ensuring success
pub fn handle_action_response(
    action: &str,
    call_result: &SoapCallResult,
) -> Result<(), ControlPointError> {
    ensure_success(action, call_result)
}

pub fn extract_child_text(
    parent: &xmltree::Element,
    suffix: &str,
) -> Result<String, ControlPointError> {
    let child = find_child_with_suffix(parent, suffix)
        .ok_or_else(|| ControlPointError::UpnpMissingReturnValue(suffix.to_string()))?;

    let text = child
        .get_text()
        .map(|t| t.trim().to_string())
        .filter(|t| !t.is_empty())
        .ok_or_else(|| ControlPointError::UpnpMissingReturnValue(suffix.to_string()))?;

    Ok(text)
}

pub fn extract_child_text_allow_empty(
    parent: &xmltree::Element,
    suffix: &str,
) -> Result<String, ControlPointError> {
    let child = find_child_with_suffix(parent, suffix)
        .ok_or_else(|| ControlPointError::UpnpMissingReturnValue(suffix.to_string()))?;

    let text = child
        .get_text()
        .map(|t| t.trim().to_string())
        .unwrap_or_default();

    Ok(text)
}

pub fn extract_child_text_optional(
    parent: &xmltree::Element,
    suffix: &str,
) -> Result<Option<String>> {
    if let Some(child) = find_child_with_suffix(parent, suffix) {
        let text = child
            .get_text()
            .map(|t| t.trim().to_string())
            .unwrap_or_default();
        Ok(Some(text))
    } else {
        Ok(None)
    }
}

pub fn extract_child_text_any(
    parent: &xmltree::Element,
    suffixes: &[&str],
) -> Result<String, ControlPointError> {
    for suffix in suffixes {
        if let Ok(text) = extract_child_text(parent, suffix) {
            return Ok(text);
        }
    }
    Err(ControlPointError::UpnpMissingReturnValue(
        suffixes.join(" or "),
    ))
}

pub fn extract_child_text_local(
    parent: &xmltree::Element,
    local: &str,
) -> Result<String, ControlPointError> {
    let child = find_child_with_local_name(parent, local).ok_or_else(|| {
        ControlPointError::SoapAction(format!("Missing {local} element in response"))
    })?;
    let text = child
        .get_text()
        .map(|t| t.trim().to_string())
        .ok_or_else(|| {
            ControlPointError::SoapAction(format!("{local} element missing text in response"))
        })?;
    Ok(text)
}

pub fn extract_child_text_optional_local(
    parent: &xmltree::Element,
    local: &str,
) -> Result<Option<String>, ControlPointError> {
    if let Some(child) = find_child_with_local_name(parent, local) {
        let text = child
            .get_text()
            .map(|t| t.trim().to_string())
            .unwrap_or_default();
        Ok(Some(text))
    } else {
        Ok(None)
    }
}

pub fn parse_visible_flag(value: &str) -> bool {
    let trimmed = value.trim();
    if trimmed.eq_ignore_ascii_case("true") {
        return true;
    }
    if trimmed.eq_ignore_ascii_case("false") {
        return false;
    }
    trimmed == "1"
}

pub fn parse_bool(value: &str) -> bool {
    value.trim() == "1"
}

pub fn decode_base64(input: &str) -> Result<Vec<u8>, ControlPointError> {
    fn value(byte: u8) -> Option<u8> {
        match byte {
            b'A'..=b'Z' => Some(byte - b'A'),
            b'a'..=b'z' => Some(byte - b'a' + 26),
            b'0'..=b'9' => Some(byte - b'0' + 52),
            b'+' => Some(62),
            b'/' => Some(63),
            _ => None,
        }
    }

    let mut output = Vec::new();
    let mut buffer: u32 = 0;
    let mut bits_collected: u8 = 0;

    for byte in input.bytes() {
        if byte == b'=' {
            break;
        }
        if byte == b'\r' || byte == b'\n' || byte == b' ' || byte == b'\t' {
            continue;
        }
        let val = value(byte).ok_or_else(|| {
            ControlPointError::ParsingError(format!("Invalid base64 character '{}'", byte as char))
        })?;
        buffer = (buffer << 6) | (val as u32);
        bits_collected += 6;
        if bits_collected >= 8 {
            bits_collected -= 8;
            let out = (buffer >> bits_collected) & 0xFF;
            output.push(out as u8);
        }
    }

    Ok(output)
}

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

    #[test]
    fn build_body_preserves_openhome_argument_names() {
        let args = [
            ("AfterId", "0"),
            ("Uri", "http://example.test/audio.flac"),
            ("Metadata", "<DIDL-Lite/>"),
        ];
        let xml =
            build_soap_body("Insert", "urn:av-openhome-org:service:Playlist:1", &args).unwrap();
        assert!(xml.contains("<AfterId>0</AfterId>"));
        assert!(xml.contains("<Uri>http://example.test/audio.flac</Uri>"));
        assert!(xml.contains("<Metadata>"));
        assert!(xml.contains("</Metadata>"));
        assert!(xml.contains("DIDL-Lite"));
        assert!(!xml.contains("<aAfterId>"));
    }
}
