//! SIP message parsing, serialization, inspection, mutation, and factory methods.
//!
//! Ported from ts/sip/message.ts.

use crate::helpers::{generate_branch, generate_call_id, generate_tag};

/// A parsed SIP message (request or response).
#[derive(Debug, Clone)]
pub struct SipMessage {
    pub start_line: String,
    pub headers: Vec<(String, String)>,
    pub body: String,
}

impl SipMessage {
    pub fn new(start_line: String, headers: Vec<(String, String)>, body: String) -> Self {
        Self {
            start_line,
            headers,
            body,
        }
    }

    // ---- Parsing -----------------------------------------------------------

    /// Parse a raw buffer into a SipMessage. Returns None for invalid data.
    pub fn parse(buf: &[u8]) -> Option<Self> {
        if buf.is_empty() {
            return None;
        }
        // First byte must be ASCII A-z.
        if buf[0] < 0x41 || buf[0] > 0x7a {
            return None;
        }

        let text = std::str::from_utf8(buf).ok()?;

        let (head, body) = if let Some(sep) = text.find("\r\n\r\n") {
            (&text[..sep], &text[sep + 4..])
        } else if let Some(sep) = text.find("\n\n") {
            (&text[..sep], &text[sep + 2..])
        } else {
            (text, "")
        };

        let normalized = head.replace("\r\n", "\n");
        let lines: Vec<&str> = normalized.split('\n').collect();
        if lines.is_empty() || lines[0].is_empty() {
            return None;
        }

        let start_line = lines[0];
        // Validate: must be a SIP request or response start line.
        if !is_sip_first_line(start_line) {
            return None;
        }

        let mut headers = Vec::new();
        for &line in &lines[1..] {
            let trimmed = line.trim();
            if trimmed.is_empty() {
                continue;
            }
            if let Some(colon) = line.find(':') {
                let name = line[..colon].trim().to_string();
                let value = line[colon + 1..].trim().to_string();
                headers.push((name, value));
            }
        }

        Some(SipMessage {
            start_line: start_line.to_string(),
            headers,
            body: body.to_string(),
        })
    }

    // ---- Serialization -----------------------------------------------------

    /// Serialize the message to a byte buffer suitable for UDP transmission.
    pub fn serialize(&self) -> Vec<u8> {
        let mut head = self.start_line.clone();
        for (name, value) in &self.headers {
            head.push_str("\r\n");
            head.push_str(name);
            head.push_str(": ");
            head.push_str(value);
        }
        head.push_str("\r\n\r\n");

        let mut buf = head.into_bytes();
        if !self.body.is_empty() {
            buf.extend_from_slice(self.body.as_bytes());
        }
        buf
    }

    // ---- Inspectors --------------------------------------------------------

    pub fn is_request(&self) -> bool {
        !self.start_line.starts_with("SIP/")
    }

    pub fn is_response(&self) -> bool {
        self.start_line.starts_with("SIP/")
    }

    /// Request method (INVITE, REGISTER, ...) or None for responses.
    pub fn method(&self) -> Option<&str> {
        if !self.is_request() {
            return None;
        }
        self.start_line.split_whitespace().next()
    }

    /// Response status code or None for requests.
    pub fn status_code(&self) -> Option<u16> {
        if !self.is_response() {
            return None;
        }
        self.start_line
            .split_whitespace()
            .nth(1)
            .and_then(|s| s.parse().ok())
    }

    pub fn call_id(&self) -> &str {
        self.get_header("Call-ID").unwrap_or("noid")
    }

    /// Method from the CSeq header (e.g. "INVITE").
    pub fn cseq_method(&self) -> Option<&str> {
        let cseq = self.get_header("CSeq")?;
        cseq.split_whitespace().nth(1)
    }

    /// True for INVITE, SUBSCRIBE, REFER, NOTIFY, UPDATE.
    pub fn is_dialog_establishing(&self) -> bool {
        matches!(
            self.method(),
            Some("INVITE" | "SUBSCRIBE" | "REFER" | "NOTIFY" | "UPDATE")
        )
    }

    /// True when the body carries an SDP payload.
    pub fn has_sdp_body(&self) -> bool {
        if self.body.is_empty() {
            return false;
        }
        let ct = self.get_header("Content-Type").unwrap_or("");
        ct.to_ascii_lowercase().starts_with("application/sdp")
    }

    // ---- Header accessors --------------------------------------------------

    /// Get the first header value matching `name` (case-insensitive).
    pub fn get_header(&self, name: &str) -> Option<&str> {
        let nl = name.to_ascii_lowercase();
        for (n, v) in &self.headers {
            if n.to_ascii_lowercase() == nl {
                return Some(v.as_str());
            }
        }
        None
    }

    /// Overwrites the first header with the given name, or appends it.
    pub fn set_header(&mut self, name: &str, value: &str) -> &mut Self {
        let nl = name.to_ascii_lowercase();
        for h in &mut self.headers {
            if h.0.to_ascii_lowercase() == nl {
                h.1 = value.to_string();
                return self;
            }
        }
        self.headers.push((name.to_string(), value.to_string()));
        self
    }

    /// Inserts a header at the top of the header list.
    pub fn prepend_header(&mut self, name: &str, value: &str) -> &mut Self {
        self.headers
            .insert(0, (name.to_string(), value.to_string()));
        self
    }

    /// Removes all headers with the given name.
    pub fn remove_header(&mut self, name: &str) -> &mut Self {
        let nl = name.to_ascii_lowercase();
        self.headers.retain(|(n, _)| n.to_ascii_lowercase() != nl);
        self
    }

    /// Recalculates Content-Length to match the current body.
    pub fn update_content_length(&mut self) -> &mut Self {
        let len = self.body.len();
        self.set_header("Content-Length", &len.to_string())
    }

    // ---- Start-line mutation -----------------------------------------------

    /// Replace the Request-URI (second token) of a request start line.
    pub fn set_request_uri(&mut self, uri: &str) -> &mut Self {
        if !self.is_request() {
            return self;
        }
        let parts: Vec<&str> = self.start_line.splitn(3, ' ').collect();
        if parts.len() >= 3 {
            self.start_line = format!("{} {} {}", parts[0], uri, parts[2]);
        }
        self
    }

    /// Returns the Request-URI (second token) of a request start line.
    pub fn request_uri(&self) -> Option<&str> {
        if !self.is_request() {
            return None;
        }
        self.start_line.split_whitespace().nth(1)
    }

    // ---- Factory methods ---------------------------------------------------

    /// Build a new SIP request.
    pub fn create_request(method: &str, request_uri: &str, opts: RequestOptions) -> Self {
        let branch = opts.via_branch.unwrap_or_else(|| generate_branch());
        let transport = opts.via_transport.unwrap_or_else(|| "UDP".to_string());
        let from_tag = opts.from_tag.unwrap_or_else(|| generate_tag());
        let call_id = opts.call_id.unwrap_or_else(|| generate_call_id(None));
        let cseq = opts.cseq.unwrap_or(1);
        let max_forwards = opts.max_forwards.unwrap_or(70);

        let from_display = opts
            .from_display_name
            .map(|d| format!("\"{d}\" "))
            .unwrap_or_default();
        let to_display = opts
            .to_display_name
            .map(|d| format!("\"{d}\" "))
            .unwrap_or_default();
        let to_tag_str = opts.to_tag.map(|t| format!(";tag={t}")).unwrap_or_default();

        let mut headers = vec![
            (
                "Via".to_string(),
                format!(
                    "SIP/2.0/{transport} {}:{};branch={branch};rport",
                    opts.via_host, opts.via_port
                ),
            ),
            (
                "From".to_string(),
                format!("{from_display}<{}>;tag={from_tag}", opts.from_uri),
            ),
            (
                "To".to_string(),
                format!("{to_display}<{}>{to_tag_str}", opts.to_uri),
            ),
            ("Call-ID".to_string(), call_id),
            ("CSeq".to_string(), format!("{cseq} {method}")),
            ("Max-Forwards".to_string(), max_forwards.to_string()),
        ];

        if let Some(contact) = &opts.contact {
            headers.push(("Contact".to_string(), contact.clone()));
        }

        if let Some(extra) = opts.extra_headers {
            headers.extend(extra);
        }

        let body = opts.body.unwrap_or_default();
        if !body.is_empty() {
            if let Some(ct) = &opts.content_type {
                headers.push(("Content-Type".to_string(), ct.clone()));
            }
        }
        headers.push(("Content-Length".to_string(), body.len().to_string()));

        SipMessage {
            start_line: format!("{method} {request_uri} SIP/2.0"),
            headers,
            body,
        }
    }

    /// Build a SIP response to an incoming request.
    /// Copies Via, From, To, Call-ID, and CSeq from the original request.
    pub fn create_response(
        status_code: u16,
        reason_phrase: &str,
        request: &SipMessage,
        opts: Option<ResponseOptions>,
    ) -> Self {
        let opts = opts.unwrap_or_default();
        let mut headers: Vec<(String, String)> = Vec::new();

        // Copy all Via headers (order matters).
        for (n, v) in &request.headers {
            if n.to_ascii_lowercase() == "via" {
                headers.push(("Via".to_string(), v.clone()));
            }
        }

        // From — copied verbatim.
        if let Some(from) = request.get_header("From") {
            headers.push(("From".to_string(), from.to_string()));
        }

        // To — add tag if provided and not already present.
        let mut to = request.get_header("To").unwrap_or("").to_string();
        if let Some(tag) = &opts.to_tag {
            if !to.contains("tag=") {
                to.push_str(&format!(";tag={tag}"));
            }
        }
        headers.push(("To".to_string(), to));

        headers.push(("Call-ID".to_string(), request.call_id().to_string()));

        if let Some(cseq) = request.get_header("CSeq") {
            headers.push(("CSeq".to_string(), cseq.to_string()));
        }

        if let Some(contact) = &opts.contact {
            headers.push(("Contact".to_string(), contact.clone()));
        }

        if let Some(extra) = opts.extra_headers {
            headers.extend(extra);
        }

        let body = opts.body.unwrap_or_default();
        if !body.is_empty() {
            if let Some(ct) = &opts.content_type {
                headers.push(("Content-Type".to_string(), ct.clone()));
            }
        }
        headers.push(("Content-Length".to_string(), body.len().to_string()));

        SipMessage {
            start_line: format!("SIP/2.0 {status_code} {reason_phrase}"),
            headers,
            body,
        }
    }

    /// Extract the tag from a From or To header value.
    pub fn extract_tag(header_value: &str) -> Option<&str> {
        let idx = header_value.find(";tag=")?;
        let rest = &header_value[idx + 5..];
        let end = rest
            .find(|c: char| c.is_whitespace() || c == ';' || c == '>')
            .unwrap_or(rest.len());
        Some(&rest[..end])
    }

    /// Extract the URI from an addr-spec or name-addr (From/To/Contact).
    pub fn extract_uri(header_value: &str) -> Option<&str> {
        if let Some(start) = header_value.find('<') {
            let end = header_value[start..].find('>')?;
            Some(&header_value[start + 1..start + end])
        } else {
            let trimmed = header_value.trim();
            let end = trimmed
                .find(|c: char| c == ';' || c == '>')
                .unwrap_or(trimmed.len());
            let result = &trimmed[..end];
            if result.is_empty() {
                None
            } else {
                Some(result)
            }
        }
    }

    /// Extract the user part from a SIP/TEL URI or header value.
    pub fn extract_uri_user(uri_or_header_value: &str) -> Option<&str> {
        let raw = Self::extract_uri(uri_or_header_value).unwrap_or(uri_or_header_value);
        let raw = raw.trim();
        if raw.is_empty() {
            return None;
        }

        let user_part = if raw
            .get(..5)
            .is_some_and(|prefix| prefix.eq_ignore_ascii_case("sips:"))
        {
            &raw[5..]
        } else if raw.get(..4).is_some_and(|prefix| {
            prefix.eq_ignore_ascii_case("sip:") || prefix.eq_ignore_ascii_case("tel:")
        }) {
            &raw[4..]
        } else {
            raw
        };

        let end = user_part
            .find(|c: char| matches!(c, '@' | ';' | '?' | '>'))
            .unwrap_or(user_part.len());
        let result = &user_part[..end];
        if result.is_empty() {
            None
        } else {
            Some(result)
        }
    }
}

/// Options for `SipMessage::create_request`.
pub struct RequestOptions {
    pub via_host: String,
    pub via_port: u16,
    pub via_transport: Option<String>,
    pub via_branch: Option<String>,
    pub from_uri: String,
    pub from_display_name: Option<String>,
    pub from_tag: Option<String>,
    pub to_uri: String,
    pub to_display_name: Option<String>,
    pub to_tag: Option<String>,
    pub call_id: Option<String>,
    pub cseq: Option<u32>,
    pub contact: Option<String>,
    pub max_forwards: Option<u16>,
    pub body: Option<String>,
    pub content_type: Option<String>,
    pub extra_headers: Option<Vec<(String, String)>>,
}

/// Options for `SipMessage::create_response`.
#[derive(Default)]
pub struct ResponseOptions {
    pub to_tag: Option<String>,
    pub contact: Option<String>,
    pub body: Option<String>,
    pub content_type: Option<String>,
    pub extra_headers: Option<Vec<(String, String)>>,
}

/// Check if a string matches the SIP first-line pattern.
fn is_sip_first_line(line: &str) -> bool {
    // Request: METHOD SP URI SP SIP/X.Y
    // Response: SIP/X.Y SP STATUS SP REASON
    if line.starts_with("SIP/") {
        // Response: SIP/2.0 200 OK
        let parts: Vec<&str> = line.splitn(3, ' ').collect();
        if parts.len() >= 2 {
            return parts[1].chars().all(|c| c.is_ascii_digit());
        }
    } else {
        // Request: INVITE sip:user@host SIP/2.0
        let parts: Vec<&str> = line.splitn(3, ' ').collect();
        if parts.len() >= 3 {
            return parts[0].chars().all(|c| c.is_ascii_uppercase())
                && parts[2].starts_with("SIP/");
        }
    }
    false
}

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

    const INVITE_RAW: &str = "INVITE sip:user@host SIP/2.0\r\n\
        Via: SIP/2.0/UDP 192.168.1.1:5060;branch=z9hG4bK-test\r\n\
        From: <sip:caller@host>;tag=abc\r\n\
        To: <sip:user@host>\r\n\
        Call-ID: test-call-id\r\n\
        CSeq: 1 INVITE\r\n\
        Content-Length: 0\r\n\r\n";

    #[test]
    fn parse_invite() {
        let msg = SipMessage::parse(INVITE_RAW.as_bytes()).unwrap();
        assert!(msg.is_request());
        assert!(!msg.is_response());
        assert_eq!(msg.method(), Some("INVITE"));
        assert_eq!(msg.call_id(), "test-call-id");
        assert_eq!(msg.cseq_method(), Some("INVITE"));
        assert!(msg.is_dialog_establishing());
        assert_eq!(msg.request_uri(), Some("sip:user@host"));
    }

    #[test]
    fn parse_response() {
        let raw = "SIP/2.0 200 OK\r\n\
            Via: SIP/2.0/UDP 192.168.1.1:5060;branch=z9hG4bK-test\r\n\
            From: <sip:caller@host>;tag=abc\r\n\
            To: <sip:user@host>;tag=def\r\n\
            Call-ID: test-call-id\r\n\
            CSeq: 1 INVITE\r\n\
            Content-Length: 0\r\n\r\n";
        let msg = SipMessage::parse(raw.as_bytes()).unwrap();
        assert!(msg.is_response());
        assert_eq!(msg.status_code(), Some(200));
        assert_eq!(msg.cseq_method(), Some("INVITE"));
    }

    #[test]
    fn serialize_roundtrip() {
        let msg = SipMessage::parse(INVITE_RAW.as_bytes()).unwrap();
        let serialized = msg.serialize();
        let reparsed = SipMessage::parse(&serialized).unwrap();
        assert_eq!(reparsed.call_id(), "test-call-id");
        assert_eq!(reparsed.method(), Some("INVITE"));
        assert_eq!(reparsed.headers.len(), msg.headers.len());
    }

    #[test]
    fn header_mutation() {
        let mut msg = SipMessage::parse(INVITE_RAW.as_bytes()).unwrap();
        msg.set_header("X-Custom", "value1");
        assert_eq!(msg.get_header("X-Custom"), Some("value1"));
        msg.set_header("X-Custom", "value2");
        assert_eq!(msg.get_header("X-Custom"), Some("value2"));
        msg.prepend_header("X-First", "first");
        assert_eq!(msg.headers[0].0, "X-First");
        msg.remove_header("X-Custom");
        assert_eq!(msg.get_header("X-Custom"), None);
    }

    #[test]
    fn set_request_uri() {
        let mut msg = SipMessage::parse(INVITE_RAW.as_bytes()).unwrap();
        msg.set_request_uri("sip:new@host");
        assert_eq!(msg.request_uri(), Some("sip:new@host"));
        assert!(msg.start_line.starts_with("INVITE sip:new@host SIP/2.0"));
    }

    #[test]
    fn extract_tag_and_uri() {
        assert_eq!(
            SipMessage::extract_tag("<sip:user@host>;tag=abc123"),
            Some("abc123")
        );
        assert_eq!(SipMessage::extract_tag("<sip:user@host>"), None);
        assert_eq!(
            SipMessage::extract_uri("<sip:user@host>"),
            Some("sip:user@host")
        );
        assert_eq!(
            SipMessage::extract_uri("\"Name\" <sip:user@host>;tag=abc"),
            Some("sip:user@host")
        );
        assert_eq!(
            SipMessage::extract_uri_user("\"Name\" <sip:+49 421 219694@host>;tag=abc"),
            Some("+49 421 219694")
        );
        assert_eq!(
            SipMessage::extract_uri_user("sip:0049421219694@voip.easybell.de"),
            Some("0049421219694")
        );
        assert_eq!(
            SipMessage::extract_uri_user("tel:+49421219694;phone-context=example.com"),
            Some("+49421219694")
        );
        assert_eq!(SipMessage::extract_uri_user("SIP:user@host"), Some("user"));
    }

    #[test]
    fn create_request_and_response() {
        let invite = SipMessage::create_request(
            "INVITE",
            "sip:user@host",
            RequestOptions {
                via_host: "192.168.1.1".to_string(),
                via_port: 5070,
                via_transport: None,
                via_branch: None,
                from_uri: "sip:caller@proxy".to_string(),
                from_display_name: None,
                from_tag: Some("mytag".to_string()),
                to_uri: "sip:user@host".to_string(),
                to_display_name: None,
                to_tag: None,
                call_id: Some("test-123".to_string()),
                cseq: Some(1),
                contact: Some("<sip:caller@192.168.1.1:5070>".to_string()),
                max_forwards: None,
                body: None,
                content_type: None,
                extra_headers: None,
            },
        );
        assert_eq!(invite.method(), Some("INVITE"));
        assert_eq!(invite.call_id(), "test-123");
        assert!(invite
            .get_header("Via")
            .unwrap()
            .contains("192.168.1.1:5070"));

        let response = SipMessage::create_response(
            200,
            "OK",
            &invite,
            Some(ResponseOptions {
                to_tag: Some("remotetag".to_string()),
                ..Default::default()
            }),
        );
        assert!(response.is_response());
        assert_eq!(response.status_code(), Some(200));
        let to = response.get_header("To").unwrap();
        assert!(to.contains("tag=remotetag"));
    }

    #[test]
    fn has_sdp_body() {
        let mut msg = SipMessage::parse(INVITE_RAW.as_bytes()).unwrap();
        assert!(!msg.has_sdp_body());
        msg.body = "v=0\r\no=- 1 1 IN IP4 0.0.0.0\r\n".to_string();
        msg.set_header("Content-Type", "application/sdp");
        assert!(msg.has_sdp_body());
    }
}
