//! SIP dialog state machine (RFC 3261 §12).
//!
//! Tracks local/remote tags, CSeq counters, route set, and remote target.
//! Provides methods to build in-dialog requests (BYE, re-INVITE, ACK, CANCEL).
//!
//! Ported from ts/sip/dialog.ts.

use crate::helpers::{generate_branch, generate_tag};
use crate::message::SipMessage;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DialogState {
    Early,
    Confirmed,
    Terminated,
}

/// SIP dialog state per RFC 3261 §12.
#[derive(Debug, Clone)]
pub struct SipDialog {
    pub call_id: String,
    pub local_tag: String,
    pub remote_tag: Option<String>,
    pub local_uri: String,
    pub remote_uri: String,
    pub local_cseq: u32,
    pub remote_cseq: u32,
    pub route_set: Vec<String>,
    pub remote_target: String,
    pub state: DialogState,
    pub local_host: String,
    pub local_port: u16,
}

impl SipDialog {
    /// Create a dialog from an INVITE we are sending (UAC side).
    /// The dialog enters Early state; call `process_response()` when responses arrive.
    pub fn from_uac_invite(invite: &SipMessage, local_host: &str, local_port: u16) -> Self {
        let from = invite.get_header("From").unwrap_or("");
        let to = invite.get_header("To").unwrap_or("");

        let local_cseq = invite
            .get_header("CSeq")
            .and_then(|c| c.split_whitespace().next())
            .and_then(|s| s.parse().ok())
            .unwrap_or(1);

        Self {
            call_id: invite.call_id().to_string(),
            local_tag: SipMessage::extract_tag(from)
                .map(|s| s.to_string())
                .unwrap_or_else(generate_tag),
            remote_tag: None,
            local_uri: SipMessage::extract_uri(from).unwrap_or("").to_string(),
            remote_uri: SipMessage::extract_uri(to).unwrap_or("").to_string(),
            local_cseq,
            remote_cseq: 0,
            route_set: Vec::new(),
            remote_target: invite
                .request_uri()
                .or_else(|| SipMessage::extract_uri(to))
                .unwrap_or("")
                .to_string(),
            state: DialogState::Early,
            local_host: local_host.to_string(),
            local_port,
        }
    }

    /// Create a dialog from an INVITE we received (UAS side).
    pub fn from_uas_invite(
        invite: &SipMessage,
        local_tag: &str,
        local_host: &str,
        local_port: u16,
    ) -> Self {
        let from = invite.get_header("From").unwrap_or("");
        let to = invite.get_header("To").unwrap_or("");
        let contact = invite.get_header("Contact");

        let remote_target = contact
            .and_then(SipMessage::extract_uri)
            .or_else(|| SipMessage::extract_uri(from))
            .unwrap_or("")
            .to_string();

        Self {
            call_id: invite.call_id().to_string(),
            local_tag: local_tag.to_string(),
            remote_tag: SipMessage::extract_tag(from).map(|s| s.to_string()),
            local_uri: SipMessage::extract_uri(to).unwrap_or("").to_string(),
            remote_uri: SipMessage::extract_uri(from).unwrap_or("").to_string(),
            local_cseq: 0,
            remote_cseq: 0,
            route_set: Vec::new(),
            remote_target,
            state: DialogState::Early,
            local_host: local_host.to_string(),
            local_port,
        }
    }

    /// Update dialog state from a received response.
    pub fn process_response(&mut self, response: &SipMessage) {
        let to = response.get_header("To").unwrap_or("");
        let tag = SipMessage::extract_tag(to).map(|s| s.to_string());
        let code = response.status_code().unwrap_or(0);

        // Always update remoteTag from 2xx (RFC 3261 §12.1.2).
        if let Some(ref t) = tag {
            if code >= 200 && code < 300 {
                self.remote_tag = Some(t.clone());
            } else if self.remote_tag.is_none() {
                self.remote_tag = Some(t.clone());
            }
        }

        // Update remote target from Contact.
        if let Some(contact) = response.get_header("Contact") {
            if let Some(uri) = SipMessage::extract_uri(contact) {
                self.remote_target = uri.to_string();
            }
        }

        // Record-Route → route set (in reverse for UAC).
        if self.state == DialogState::Early {
            let rr: Vec<String> = response
                .headers
                .iter()
                .filter(|(n, _)| n.to_ascii_lowercase() == "record-route")
                .map(|(_, v)| v.clone())
                .collect();
            if !rr.is_empty() {
                let mut reversed = rr;
                reversed.reverse();
                self.route_set = reversed;
            }
        }

        if code >= 200 && code < 300 {
            self.state = DialogState::Confirmed;
        } else if code >= 300 {
            self.state = DialogState::Terminated;
        }
    }

    /// Build an in-dialog request (BYE, re-INVITE, INFO, ...).
    /// Automatically increments the local CSeq.
    pub fn create_request(
        &mut self,
        method: &str,
        body: Option<&str>,
        content_type: Option<&str>,
        extra_headers: Option<Vec<(String, String)>>,
    ) -> SipMessage {
        self.local_cseq += 1;
        let branch = generate_branch();

        let remote_tag_str = self
            .remote_tag
            .as_ref()
            .map(|t| format!(";tag={t}"))
            .unwrap_or_default();

        let mut headers = vec![
            (
                "Via".to_string(),
                format!(
                    "SIP/2.0/UDP {}:{};branch={branch};rport",
                    self.local_host, self.local_port
                ),
            ),
            (
                "From".to_string(),
                format!("<{}>;tag={}", self.local_uri, self.local_tag),
            ),
            (
                "To".to_string(),
                format!("<{}>{remote_tag_str}", self.remote_uri),
            ),
            ("Call-ID".to_string(), self.call_id.clone()),
            ("CSeq".to_string(), format!("{} {method}", self.local_cseq)),
            ("Max-Forwards".to_string(), "70".to_string()),
        ];

        for route in &self.route_set {
            headers.push(("Route".to_string(), route.clone()));
        }

        headers.push((
            "Contact".to_string(),
            format!("<sip:{}:{}>", self.local_host, self.local_port),
        ));

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

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

        let ruri = self.resolve_ruri();
        SipMessage::new(
            format!("{method} {ruri} SIP/2.0"),
            headers,
            body_str.to_string(),
        )
    }

    /// Build an ACK for a 2xx response to INVITE (RFC 3261 §13.2.2.4).
    pub fn create_ack(&self) -> SipMessage {
        let branch = generate_branch();
        let remote_tag_str = self
            .remote_tag
            .as_ref()
            .map(|t| format!(";tag={t}"))
            .unwrap_or_default();

        let mut headers = vec![
            (
                "Via".to_string(),
                format!(
                    "SIP/2.0/UDP {}:{};branch={branch};rport",
                    self.local_host, self.local_port
                ),
            ),
            (
                "From".to_string(),
                format!("<{}>;tag={}", self.local_uri, self.local_tag),
            ),
            (
                "To".to_string(),
                format!("<{}>{remote_tag_str}", self.remote_uri),
            ),
            ("Call-ID".to_string(), self.call_id.clone()),
            ("CSeq".to_string(), format!("{} ACK", self.local_cseq)),
            ("Max-Forwards".to_string(), "70".to_string()),
        ];

        for route in &self.route_set {
            headers.push(("Route".to_string(), route.clone()));
        }

        headers.push(("Content-Length".to_string(), "0".to_string()));

        let ruri = self.resolve_ruri();
        SipMessage::new(format!("ACK {ruri} SIP/2.0"), headers, String::new())
    }

    /// Build a CANCEL for the original INVITE (same branch, CSeq).
    pub fn create_cancel(&self, original_invite: &SipMessage) -> SipMessage {
        let via = original_invite.get_header("Via").unwrap_or("").to_string();
        let from = original_invite.get_header("From").unwrap_or("").to_string();
        let to = original_invite.get_header("To").unwrap_or("").to_string();

        let headers = vec![
            ("Via".to_string(), via),
            ("From".to_string(), from),
            ("To".to_string(), to),
            ("Call-ID".to_string(), self.call_id.clone()),
            ("CSeq".to_string(), format!("{} CANCEL", self.local_cseq)),
            ("Max-Forwards".to_string(), "70".to_string()),
            ("Content-Length".to_string(), "0".to_string()),
        ];

        let ruri = original_invite
            .request_uri()
            .unwrap_or(&self.remote_target)
            .to_string();

        SipMessage::new(format!("CANCEL {ruri} SIP/2.0"), headers, String::new())
    }

    /// Transition the dialog to terminated state.
    pub fn terminate(&mut self) {
        self.state = DialogState::Terminated;
    }

    /// Resolve Request-URI from route set or remote target.
    fn resolve_ruri(&self) -> &str {
        if !self.route_set.is_empty() {
            if let Some(top_route) = SipMessage::extract_uri(&self.route_set[0]) {
                if top_route.contains(";lr") {
                    return &self.remote_target; // loose routing
                }
                return top_route; // strict routing
            }
        }
        &self.remote_target
    }
}

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

    fn make_invite() -> SipMessage {
        SipMessage::create_request(
            "INVITE",
            "sip:callee@host",
            RequestOptions {
                via_host: "192.168.1.1".to_string(),
                via_port: 5070,
                via_transport: None,
                via_branch: Some("z9hG4bK-test".to_string()),
                from_uri: "sip:caller@proxy".to_string(),
                from_display_name: None,
                from_tag: Some("from-tag".to_string()),
                to_uri: "sip:callee@host".to_string(),
                to_display_name: None,
                to_tag: None,
                call_id: Some("test-dialog-call".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,
            },
        )
    }

    #[test]
    fn uac_dialog_lifecycle() {
        let invite = make_invite();
        let mut dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);

        assert_eq!(dialog.state, DialogState::Early);
        assert_eq!(dialog.call_id, "test-dialog-call");
        assert_eq!(dialog.local_tag, "from-tag");
        assert!(dialog.remote_tag.is_none());

        // Simulate 200 OK
        let response = SipMessage::create_response(
            200,
            "OK",
            &invite,
            Some(crate::message::ResponseOptions {
                to_tag: Some("remote-tag".to_string()),
                contact: Some("<sip:callee@10.0.0.1:5060>".to_string()),
                ..Default::default()
            }),
        );

        dialog.process_response(&response);
        assert_eq!(dialog.state, DialogState::Confirmed);
        assert_eq!(dialog.remote_tag.as_deref(), Some("remote-tag"));
        assert_eq!(dialog.remote_target, "sip:callee@10.0.0.1:5060");
    }

    #[test]
    fn create_bye() {
        let invite = make_invite();
        let mut dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);
        dialog.remote_tag = Some("remote-tag".to_string());
        dialog.state = DialogState::Confirmed;

        let bye = dialog.create_request("BYE", None, None, None);
        assert_eq!(bye.method(), Some("BYE"));
        assert_eq!(bye.call_id(), "test-dialog-call");
        assert_eq!(dialog.local_cseq, 2);
        let to = bye.get_header("To").unwrap();
        assert!(to.contains("tag=remote-tag"));
    }

    #[test]
    fn create_ack() {
        let invite = make_invite();
        let mut dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);
        dialog.remote_tag = Some("remote-tag".to_string());

        let ack = dialog.create_ack();
        assert_eq!(ack.method(), Some("ACK"));
        assert!(ack.get_header("CSeq").unwrap().contains("ACK"));
    }

    #[test]
    fn create_cancel() {
        let invite = make_invite();
        let dialog = SipDialog::from_uac_invite(&invite, "192.168.1.1", 5070);

        let cancel = dialog.create_cancel(&invite);
        assert_eq!(cancel.method(), Some("CANCEL"));
        assert!(cancel.get_header("CSeq").unwrap().contains("CANCEL"));
        assert!(cancel.start_line.contains("sip:callee@host"));
    }
}
