//! SipLeg — manages one side of a B2BUA call.
//!
//! Handles the full INVITE lifecycle:
//! - Send INVITE with SDP
//! - Handle 407 Proxy Authentication (digest auth retry)
//! - Handle 200 OK (ACK, learn media endpoint)
//! - Handle BYE/CANCEL (teardown)
//! - Track SIP dialog state (early → confirmed → terminated)
//!
//! Ported from ts/call/sip-leg.ts.

use sip_proto::dialog::{DialogState, SipDialog};
use sip_proto::helpers::{
    build_sdp, compute_digest_auth, generate_branch, generate_tag, parse_digest_challenge,
    parse_sdp_endpoint, SdpOptions,
};
use sip_proto::message::{RequestOptions, SipMessage};
use std::net::SocketAddr;
use tokio::net::UdpSocket;

/// State of a SIP leg.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LegState {
    Inviting,
    Ringing,
    Connected,
    Terminating,
    Terminated,
}

/// Configuration for creating a SIP leg.
pub struct SipLegConfig {
    /// Proxy LAN IP (for Via, Contact, SDP).
    pub lan_ip: String,
    /// Proxy LAN port.
    pub lan_port: u16,
    /// Public IP (for provider-facing legs).
    pub public_ip: Option<String>,
    /// SIP target endpoint (provider outbound proxy or device address).
    pub sip_target: SocketAddr,
    /// Provider credentials (for 407 auth).
    // username is carried for parity with the provider config but digest auth
    // rebuilds the username from the registered AOR, so this slot is never read.
    #[allow(dead_code)]
    pub username: Option<String>,
    pub password: Option<String>,
    pub registered_aor: Option<String>,
    /// Codec payload types to offer.
    pub codecs: Vec<u8>,
    /// Our RTP port for SDP.
    pub rtp_port: u16,
}

/// A SIP leg with full dialog management.
pub struct SipLeg {
    // Leg identity is tracked via the enclosing LegInfo's key in the call's
    // leg map; SipLeg itself never reads this field back. Kept to preserve
    // the (id, config) constructor shape used by the call manager.
    #[allow(dead_code)]
    pub id: String,
    pub state: LegState,
    pub config: SipLegConfig,
    pub dialog: Option<SipDialog>,

    /// The INVITE we sent (needed for CANCEL and 407 ACK).
    invite: Option<SipMessage>,
    /// Original unauthenticated INVITE (for re-ACKing retransmitted 407s).
    orig_invite: Option<SipMessage>,
    /// Whether we've attempted digest auth.
    auth_attempted: bool,

    /// Remote media endpoint (learned from SDP in 200 OK).
    pub remote_media: Option<SocketAddr>,
}

impl SipLeg {
    pub fn new(id: String, config: SipLegConfig) -> Self {
        Self {
            id,
            state: LegState::Inviting,
            config,
            dialog: None,
            invite: None,
            orig_invite: None,
            auth_attempted: false,
            remote_media: None,
        }
    }

    /// Build and send an INVITE to establish this leg.
    pub async fn send_invite(
        &mut self,
        from_uri: &str,
        to_uri: &str,
        sip_call_id: &str,
        socket: &UdpSocket,
    ) {
        let ip = self
            .config
            .public_ip
            .as_deref()
            .unwrap_or(&self.config.lan_ip);

        let sdp = build_sdp(&SdpOptions {
            ip,
            port: self.config.rtp_port,
            payload_types: &self.config.codecs,
            ..Default::default()
        });

        self.send_invite_with_sdp(from_uri, to_uri, sip_call_id, socket, sdp)
            .await;
    }

    pub async fn send_invite_with_sdp(
        &mut self,
        from_uri: &str,
        to_uri: &str,
        sip_call_id: &str,
        socket: &UdpSocket,
        sdp: String,
    ) {
        let ip = self
            .config
            .public_ip
            .as_deref()
            .unwrap_or(&self.config.lan_ip);

        let invite = SipMessage::create_request(
            "INVITE",
            to_uri,
            RequestOptions {
                via_host: ip.to_string(),
                via_port: self.config.lan_port,
                via_transport: None,
                via_branch: Some(generate_branch()),
                from_uri: from_uri.to_string(),
                from_display_name: None,
                from_tag: Some(generate_tag()),
                to_uri: to_uri.to_string(),
                to_display_name: None,
                to_tag: None,
                call_id: Some(sip_call_id.to_string()),
                cseq: Some(1),
                contact: Some(format!("<sip:{ip}:{}>", self.config.lan_port)),
                max_forwards: Some(70),
                body: Some(sdp),
                content_type: Some("application/sdp".to_string()),
                extra_headers: Some(vec![(
                    "User-Agent".to_string(),
                    "SipRouter/1.0".to_string(),
                )]),
            },
        );

        self.dialog = Some(SipDialog::from_uac_invite(
            &invite,
            ip,
            self.config.lan_port,
        ));
        self.invite = Some(invite.clone());
        self.state = LegState::Inviting;

        let _ = socket
            .send_to(&invite.serialize(), self.config.sip_target)
            .await;
    }

    /// Handle an incoming SIP message routed to this leg.
    /// Returns an optional reply to send (e.g. ACK, auth retry INVITE).
    pub fn handle_message(&mut self, msg: &SipMessage) -> SipLegAction {
        if msg.is_response() {
            self.handle_response(msg)
        } else {
            self.handle_request(msg)
        }
    }

    fn handle_response(&mut self, msg: &SipMessage) -> SipLegAction {
        let code = msg.status_code().unwrap_or(0);
        let cseq_method = msg.cseq_method().unwrap_or("").to_uppercase();

        if cseq_method != "INVITE" {
            return SipLegAction::None;
        }

        // Handle retransmitted 407 for the original unauthenticated INVITE.
        if self.auth_attempted {
            if let Some(dialog) = &self.dialog {
                let response_cseq: u32 = msg
                    .get_header("CSeq")
                    .and_then(|s| s.split_whitespace().next())
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(0);
                if response_cseq < dialog.local_cseq && code >= 400 {
                    // ACK the retransmitted error response.
                    if let Some(orig) = &self.orig_invite {
                        let ack = build_non_2xx_ack(orig, msg);
                        return SipLegAction::Send(ack.serialize());
                    }
                    return SipLegAction::None;
                }
            }
        }

        // Handle 407 Proxy Authentication Required.
        if code == 407 {
            return self.handle_auth_challenge(msg);
        }

        // Update dialog state.
        if let Some(dialog) = &mut self.dialog {
            dialog.process_response(msg);
        }

        if code == 180 || code == 183 {
            self.state = LegState::Ringing;
            SipLegAction::StateChange(LegState::Ringing)
        } else if code >= 200 && code < 300 {
            // ACK the 200 OK.
            let ack_buf = if let Some(dialog) = &self.dialog {
                let ack = dialog.create_ack();
                Some(ack.serialize())
            } else {
                None
            };

            // If already connected (200 retransmit), just re-ACK.
            if self.state == LegState::Connected {
                return match ack_buf {
                    Some(buf) => SipLegAction::Send(buf),
                    None => SipLegAction::None,
                };
            }

            // Learn media endpoint from SDP.
            if msg.has_sdp_body() {
                if let Some(ep) = parse_sdp_endpoint(&msg.body) {
                    if let Ok(addr) = format!("{}:{}", ep.address, ep.port).parse() {
                        self.remote_media = Some(addr);
                    }
                }
            }

            self.state = LegState::Connected;

            match ack_buf {
                Some(buf) => SipLegAction::ConnectedWithAck(buf),
                None => SipLegAction::StateChange(LegState::Connected),
            }
        } else if code >= 300 {
            self.state = LegState::Terminated;
            if let Some(dialog) = &mut self.dialog {
                dialog.terminate();
            }
            SipLegAction::Terminated(format!("rejected_{code}"))
        } else {
            SipLegAction::None // 1xx provisional
        }
    }

    fn handle_auth_challenge(&mut self, msg: &SipMessage) -> SipLegAction {
        if self.auth_attempted {
            self.state = LegState::Terminated;
            if let Some(dialog) = &mut self.dialog {
                dialog.terminate();
            }
            return SipLegAction::Terminated("auth_rejected".to_string());
        }
        self.auth_attempted = true;

        let challenge_header = match msg.get_header("Proxy-Authenticate") {
            Some(h) => h,
            None => {
                self.state = LegState::Terminated;
                return SipLegAction::Terminated("407_no_challenge".to_string());
            }
        };

        let challenge = match parse_digest_challenge(challenge_header) {
            Some(c) => c,
            None => {
                self.state = LegState::Terminated;
                return SipLegAction::Terminated("407_bad_challenge".to_string());
            }
        };

        let password = match &self.config.password {
            Some(p) => p.clone(),
            None => {
                self.state = LegState::Terminated;
                return SipLegAction::Terminated("407_no_password".to_string());
            }
        };

        let aor = match &self.config.registered_aor {
            Some(a) => a.clone(),
            None => {
                self.state = LegState::Terminated;
                return SipLegAction::Terminated("407_no_aor".to_string());
            }
        };

        let username = aor
            .trim_start_matches("sip:")
            .trim_start_matches("sips:")
            .split('@')
            .next()
            .unwrap_or("")
            .to_string();

        let dest_uri = self
            .invite
            .as_ref()
            .and_then(|i| i.request_uri())
            .unwrap_or("")
            .to_string();

        let auth_value = compute_digest_auth(
            &username,
            &password,
            &challenge.realm,
            &challenge.nonce,
            "INVITE",
            &dest_uri,
            challenge.algorithm.as_deref(),
            challenge.opaque.as_deref(),
        );

        // ACK the 407.
        let mut ack_buf = None;
        if let Some(invite) = &self.invite {
            let ack = build_non_2xx_ack(invite, msg);
            ack_buf = Some(ack.serialize());
        }

        // Save original INVITE for retransmission handling.
        self.orig_invite = self.invite.clone();

        // Build authenticated INVITE with same From tag, CSeq=2.
        let ip = self
            .config
            .public_ip
            .as_deref()
            .unwrap_or(&self.config.lan_ip);
        let from_tag = self
            .dialog
            .as_ref()
            .map(|d| d.local_tag.clone())
            .unwrap_or_else(generate_tag);

        let sdp = build_sdp(&SdpOptions {
            ip,
            port: self.config.rtp_port,
            payload_types: &self.config.codecs,
            ..Default::default()
        });

        let call_id = self
            .dialog
            .as_ref()
            .map(|d| d.call_id.clone())
            .unwrap_or_default();

        let invite_auth = SipMessage::create_request(
            "INVITE",
            &dest_uri,
            RequestOptions {
                via_host: ip.to_string(),
                via_port: self.config.lan_port,
                via_transport: None,
                via_branch: Some(generate_branch()),
                from_uri: aor,
                from_display_name: None,
                from_tag: Some(from_tag),
                to_uri: dest_uri.clone(),
                to_display_name: None,
                to_tag: None,
                call_id: Some(call_id),
                cseq: Some(2),
                contact: Some(format!("<sip:{ip}:{}>", self.config.lan_port)),
                max_forwards: Some(70),
                body: Some(sdp),
                content_type: Some("application/sdp".to_string()),
                extra_headers: Some(vec![
                    ("Proxy-Authorization".to_string(), auth_value),
                    ("User-Agent".to_string(), "SipRouter/1.0".to_string()),
                ]),
            },
        );

        self.invite = Some(invite_auth.clone());
        if let Some(dialog) = &mut self.dialog {
            dialog.local_cseq = 2;
        }

        // Return both the ACK for the 407 and the new authenticated INVITE.
        let invite_buf = invite_auth.serialize();
        SipLegAction::AuthRetry {
            ack_407: ack_buf,
            invite_with_auth: invite_buf,
        }
    }

    fn handle_request(&mut self, msg: &SipMessage) -> SipLegAction {
        let method = msg.method().unwrap_or("");

        if method == "BYE" {
            let ok = SipMessage::create_response(200, "OK", msg, None);
            self.state = LegState::Terminated;
            if let Some(dialog) = &mut self.dialog {
                dialog.terminate();
            }
            return SipLegAction::SendAndTerminate(ok.serialize(), "bye".to_string());
        }

        if method == "INFO" {
            let ok = SipMessage::create_response(200, "OK", msg, None);
            return SipLegAction::Send(ok.serialize());
        }

        if method == "INVITE" || method == "UPDATE" {
            return SipLegAction::InDialogRequest(method.to_string());
        }

        SipLegAction::None
    }

    /// Build a BYE or CANCEL to tear down this leg.
    pub fn build_hangup(&mut self) -> Option<Vec<u8>> {
        let dialog = self.dialog.as_mut()?;

        let msg = if dialog.state == DialogState::Confirmed {
            dialog.create_request("BYE", None, None, None)
        } else if dialog.state == DialogState::Early {
            if let Some(invite) = &self.invite {
                dialog.create_cancel(invite)
            } else {
                return None;
            }
        } else {
            return None;
        };

        self.state = LegState::Terminating;
        dialog.terminate();
        Some(msg.serialize())
    }
}

/// Actions produced by the SipLeg message handler.
pub enum SipLegAction {
    /// No action needed.
    None,
    /// Send a SIP message (ACK, 200 OK to INFO, etc.).
    Send(Vec<u8>),
    /// Leg state changed.
    StateChange(LegState),
    /// Connected — send this ACK.
    ConnectedWithAck(Vec<u8>),
    /// Provider sent an in-dialog request (re-INVITE / UPDATE) that needs
    /// call-manager-specific handling.
    InDialogRequest(String),
    /// Terminated with a reason.
    Terminated(String),
    /// Send 200 OK and terminate.
    SendAndTerminate(Vec<u8>, String),
    /// 407 auth retry — send ACK for 407, then send new INVITE with auth.
    AuthRetry {
        ack_407: Option<Vec<u8>>,
        invite_with_auth: Vec<u8>,
    },
}

/// Build an ACK for a non-2xx response (same transaction as the INVITE).
fn build_non_2xx_ack(original_invite: &SipMessage, response: &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 = response.get_header("To").unwrap_or("").to_string();
    let call_id = original_invite.call_id().to_string();
    let cseq_num: u32 = original_invite
        .get_header("CSeq")
        .and_then(|s| s.split_whitespace().next())
        .and_then(|s| s.parse().ok())
        .unwrap_or(1);

    let ruri = original_invite
        .request_uri()
        .unwrap_or("sip:unknown")
        .to_string();

    SipMessage::new(
        format!("ACK {ruri} SIP/2.0"),
        vec![
            ("Via".to_string(), via),
            ("From".to_string(), from),
            ("To".to_string(), to),
            ("Call-ID".to_string(), call_id),
            ("CSeq".to_string(), format!("{cseq_num} ACK")),
            ("Max-Forwards".to_string(), "70".to_string()),
            ("Content-Length".to_string(), "0".to_string()),
        ],
        String::new(),
    )
}
