// SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
//
// SPDX-License-Identifier: GPL-2.0-or-later

//! Codec utilities for parsing and formatting modes and envelopes.

use std::borrow::Cow;

use serde_json;

use crate::types::{ClientCommand, ClientEnvelope};
use trx_core::rig::state::RigMode;

/// Parse a mode string into a RigMode.
///
/// Handles LSB, USB, CW, CWR, AM, FM, WFM, AIS, VDES, DIG, DIGI, PKT, PACKET.
/// Falls back to Other(string) for unknown modes.
pub fn parse_mode(s: &str) -> RigMode {
    match s.to_uppercase().as_str() {
        "LSB" => RigMode::LSB,
        "USB" => RigMode::USB,
        "CW" => RigMode::CW,
        "CWR" => RigMode::CWR,
        "AM" => RigMode::AM,
        "SAM" | "AMC-QUAM" | "AMC_QUAM" | "AMC" => RigMode::SAM,
        "FM" => RigMode::FM,
        "WFM" => RigMode::WFM,
        "AIS" => RigMode::AIS,
        "VDES" => RigMode::VDES,
        "DIG" | "DIGI" => RigMode::DIG,
        "PKT" | "PACKET" => RigMode::PKT,
        other => RigMode::Other(other.to_string()),
    }
}

/// Convert a RigMode back to its string representation.
///
/// This is the inverse of parse_mode. Standard modes return a borrowed
/// `&'static str` (zero allocation), while `Other` variants return the
/// owned inner string.
pub fn mode_to_string(mode: &RigMode) -> Cow<'static, str> {
    match mode {
        RigMode::LSB => Cow::Borrowed("LSB"),
        RigMode::USB => Cow::Borrowed("USB"),
        RigMode::CW => Cow::Borrowed("CW"),
        RigMode::CWR => Cow::Borrowed("CWR"),
        RigMode::AM => Cow::Borrowed("AM"),
        RigMode::SAM => Cow::Borrowed("SAM"),
        RigMode::FM => Cow::Borrowed("FM"),
        RigMode::WFM => Cow::Borrowed("WFM"),
        RigMode::AIS => Cow::Borrowed("AIS"),
        RigMode::VDES => Cow::Borrowed("VDES"),
        RigMode::DIG => Cow::Borrowed("DIG"),
        RigMode::PKT => Cow::Borrowed("PKT"),
        RigMode::Other(s) => Cow::Owned(s.clone()),
    }
}

/// Parse a JSON string into a ClientEnvelope.
///
/// First tries to parse as a full ClientEnvelope.
/// If that fails, tries to parse as a bare ClientCommand and wraps it with token: None.
/// Unknown command names are reported as errors rather than causing a parse failure,
/// enabling forward compatibility when newer clients connect to older servers.
pub fn parse_envelope(input: &str) -> Result<ClientEnvelope, serde_json::Error> {
    match serde_json::from_str::<ClientEnvelope>(input) {
        Ok(envelope) => Ok(envelope),
        Err(envelope_err) => {
            // Try bare command fallback.
            match serde_json::from_str::<ClientCommand>(input) {
                Ok(cmd) => Ok(ClientEnvelope {
                    token: None,
                    rig_id: None,
                    protocol_version: None,
                    cmd,
                }),
                Err(_) => {
                    // Check if the input is valid JSON with an unrecognised "cmd" value.
                    // Return the original envelope error for truly malformed input.
                    if let Ok(val) = serde_json::from_str::<serde_json::Value>(input) {
                        if val.get("cmd").and_then(|c| c.as_str()).is_some() {
                            return Err(envelope_err);
                        }
                    }
                    Err(envelope_err)
                }
            }
        }
    }
}

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

    #[test]
    fn test_parse_mode_standard_modes() {
        assert_eq!(parse_mode("LSB"), RigMode::LSB);
        assert_eq!(parse_mode("USB"), RigMode::USB);
        assert_eq!(parse_mode("CW"), RigMode::CW);
        assert_eq!(parse_mode("CWR"), RigMode::CWR);
        assert_eq!(parse_mode("AM"), RigMode::AM);
        assert_eq!(parse_mode("SAM"), RigMode::SAM);
        assert_eq!(parse_mode("AMC-QUAM"), RigMode::SAM);
        assert_eq!(parse_mode("FM"), RigMode::FM);
        assert_eq!(parse_mode("WFM"), RigMode::WFM);
        assert_eq!(parse_mode("AIS"), RigMode::AIS);
        assert_eq!(parse_mode("VDES"), RigMode::VDES);
    }

    #[test]
    fn test_parse_mode_aliases() {
        assert_eq!(parse_mode("DIG"), RigMode::DIG);
        assert_eq!(parse_mode("DIGI"), RigMode::DIG);
        assert_eq!(parse_mode("PKT"), RigMode::PKT);
        assert_eq!(parse_mode("PACKET"), RigMode::PKT);
    }

    #[test]
    fn test_parse_mode_case_insensitive() {
        assert_eq!(parse_mode("lsb"), RigMode::LSB);
        assert_eq!(parse_mode("Usb"), RigMode::USB);
        assert_eq!(parse_mode("cw"), RigMode::CW);
    }

    #[test]
    fn test_parse_mode_unknown() {
        if let RigMode::Other(s) = parse_mode("UNKNOWN") {
            assert_eq!(s, "UNKNOWN");
        } else {
            panic!("Expected Other variant");
        }
    }

    #[test]
    fn test_parse_mode_empty() {
        if let RigMode::Other(s) = parse_mode("") {
            assert_eq!(s, "");
        } else {
            panic!("Expected Other variant");
        }
    }

    #[test]
    fn test_mode_to_string_standard_modes() {
        assert_eq!(mode_to_string(&RigMode::LSB), "LSB");
        assert_eq!(mode_to_string(&RigMode::USB), "USB");
        assert_eq!(mode_to_string(&RigMode::CW), "CW");
        assert_eq!(mode_to_string(&RigMode::CWR), "CWR");
        assert_eq!(mode_to_string(&RigMode::AM), "AM");
        assert_eq!(mode_to_string(&RigMode::SAM), "SAM");
        assert_eq!(mode_to_string(&RigMode::FM), "FM");
        assert_eq!(mode_to_string(&RigMode::WFM), "WFM");
        assert_eq!(mode_to_string(&RigMode::AIS), "AIS");
        assert_eq!(mode_to_string(&RigMode::VDES), "VDES");
        assert_eq!(mode_to_string(&RigMode::DIG), "DIG");
        assert_eq!(mode_to_string(&RigMode::PKT), "PKT");
    }

    #[test]
    fn test_mode_to_string_other() {
        assert_eq!(mode_to_string(&RigMode::Other("XYZ".to_string())), "XYZ");
    }

    #[test]
    fn test_mode_round_trip() {
        let modes = vec![
            RigMode::LSB,
            RigMode::USB,
            RigMode::CW,
            RigMode::CWR,
            RigMode::AM,
            RigMode::SAM,
            RigMode::FM,
            RigMode::WFM,
            RigMode::AIS,
            RigMode::VDES,
            RigMode::DIG,
            RigMode::PKT,
        ];

        for mode in modes {
            let s = mode_to_string(&mode);
            let parsed = parse_mode(&s);
            assert_eq!(parsed, mode, "Round trip failed for {:?}", mode);
        }
    }

    #[test]
    fn test_parse_envelope_full_envelope() {
        let json = r#"{"token":"abc123","cmd":"get_state"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert_eq!(envelope.token, Some("abc123".to_string()));
        assert!(matches!(envelope.cmd, ClientCommand::GetState));
    }

    #[test]
    fn test_parse_envelope_bare_command() {
        let json = r#"{"cmd":"get_state"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert_eq!(envelope.token, None);
        assert!(matches!(envelope.cmd, ClientCommand::GetState));
    }

    #[test]
    fn test_parse_envelope_bare_command_with_params() {
        let json = r#"{"cmd":"set_freq","freq_hz":14100000}"#;
        let envelope = parse_envelope(json).unwrap();
        assert_eq!(envelope.token, None);
        if let ClientCommand::SetFreq { freq_hz } = envelope.cmd {
            assert_eq!(freq_hz, 14100000);
        } else {
            panic!("Expected SetFreq variant");
        }
    }

    #[test]
    fn test_parse_envelope_invalid_json() {
        let json = "not valid json";
        let result = parse_envelope(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_envelope_invalid_command() {
        let json = r#"{"cmd":"invalid_command"}"#;
        let result = parse_envelope(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_envelope_with_bearer_token() {
        let json = r#"{"token":"Bearer abc123xyz","cmd":"get_state"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert_eq!(envelope.token, Some("Bearer abc123xyz".to_string()));
    }

    // --- MR-09: multi-rig protocol tests ---

    #[test]
    fn test_parse_envelope_absent_rig_id_defaults_to_none() {
        let json = r#"{"cmd":"get_state"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert_eq!(envelope.rig_id, None, "absent rig_id should parse as None");
    }

    #[test]
    fn test_parse_envelope_with_rig_id() {
        let json = r#"{"rig_id":"hf","cmd":"get_state"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert_eq!(envelope.rig_id, Some("hf".to_string()));
        assert!(matches!(envelope.cmd, ClientCommand::GetState));
    }

    #[test]
    fn test_parse_envelope_get_rigs_command() {
        let json = r#"{"cmd":"get_rigs"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert!(matches!(envelope.cmd, ClientCommand::GetRigs));
        assert_eq!(envelope.rig_id, None);
    }

    #[test]
    fn test_parse_envelope_get_rigs_with_rig_id_ignored() {
        // rig_id is parsed and available even though GetRigs is intercepted
        // before routing — the listener should ignore it for this command.
        let json = r#"{"rig_id":"sdr","cmd":"get_rigs"}"#;
        let envelope = parse_envelope(json).unwrap();
        assert!(matches!(envelope.cmd, ClientCommand::GetRigs));
        assert_eq!(envelope.rig_id, Some("sdr".to_string()));
    }

    #[test]
    fn test_client_response_rig_id_roundtrip() {
        use crate::types::ClientResponse;
        let resp = ClientResponse {
            success: true,
            rig_id: Some("hf".to_string()),
            protocol_version: None,
            state: None,
            rigs: None,
            sat_passes: None,
            error: None,
        };
        let json = serde_json::to_string(&resp).unwrap();
        assert!(json.contains(r#""rig_id":"hf""#));
        let decoded: ClientResponse = serde_json::from_str(&json).unwrap();
        assert_eq!(decoded.rig_id, Some("hf".to_string()));
    }

    #[test]
    fn test_client_response_omits_rig_id_when_none() {
        use crate::types::ClientResponse;
        let resp = ClientResponse {
            success: false,
            rig_id: None,
            protocol_version: None,
            state: None,
            rigs: None,
            sat_passes: None,
            error: Some("bad".to_string()),
        };
        let json = serde_json::to_string(&resp).unwrap();
        assert!(
            !json.contains("rig_id"),
            "rig_id=None should be omitted from JSON"
        );
    }

    #[test]
    fn test_client_response_omits_rigs_when_none() {
        use crate::types::ClientResponse;
        let resp = ClientResponse {
            success: true,
            rig_id: Some("server".to_string()),
            protocol_version: None,
            state: None,
            rigs: None,
            sat_passes: None,
            error: None,
        };
        let json = serde_json::to_string(&resp).unwrap();
        assert!(
            !json.contains("\"rigs\""),
            "rigs=None should be omitted from JSON"
        );
    }

    // --- UC-09: filter field serialization tests ---

    #[test]
    fn filter_field_included_when_some() {
        use trx_core::rig::state::RigSnapshot;
        use trx_core::RigFilterState;
        let snap_json = serde_json::to_string(&RigSnapshot {
            filter: Some(RigFilterState {
                bandwidth_hz: 3000,
                cw_center_hz: 700,
                sdr_gain_db: Some(12.0),
                sdr_lna_gain_db: None,
                sdr_agc_enabled: None,
                sdr_squelch_enabled: None,
                sdr_squelch_threshold_db: None,
                sdr_nb_enabled: None,
                sdr_nb_threshold: None,
                wfm_deemphasis_us: 75,
                wfm_stereo: true,
                wfm_stereo_detected: false,
                wfm_denoise: trx_core::WfmDenoiseLevel::Auto,
                wfm_cci: 0,
                wfm_aci: 0,
                sam_stereo_width: 1.0,
                sam_carrier_sync: true,
            }),
            ..minimal_snapshot()
        })
        .unwrap();
        assert!(
            snap_json.contains("\"filter\""),
            "filter=Some should be serialized"
        );
        assert!(snap_json.contains("\"bandwidth_hz\":3000"));
    }

    #[test]
    fn filter_field_omitted_when_none() {
        use trx_core::rig::state::RigSnapshot;
        let snap_json = serde_json::to_string(&RigSnapshot {
            filter: None,
            ..minimal_snapshot()
        })
        .unwrap();
        assert!(
            !snap_json.contains("\"filter\""),
            "filter=None should be omitted from JSON"
        );
    }

    #[test]
    fn filter_field_roundtrips() {
        use trx_core::rig::state::RigSnapshot;
        use trx_core::RigFilterState;
        let orig = RigSnapshot {
            filter: Some(RigFilterState {
                bandwidth_hz: 12000,
                cw_center_hz: 700,
                sdr_gain_db: Some(18.0),
                sdr_lna_gain_db: None,
                sdr_agc_enabled: None,
                sdr_squelch_enabled: None,
                sdr_squelch_threshold_db: None,
                sdr_nb_enabled: None,
                sdr_nb_threshold: None,
                wfm_deemphasis_us: 50,
                wfm_stereo: true,
                wfm_stereo_detected: true,
                wfm_denoise: trx_core::WfmDenoiseLevel::Auto,
                wfm_cci: 12,
                wfm_aci: 45,
                sam_stereo_width: 0.5,
                sam_carrier_sync: false,
            }),
            ..minimal_snapshot()
        };
        let json = serde_json::to_string(&orig).unwrap();
        let decoded: RigSnapshot = serde_json::from_str(&json).unwrap();
        let f = decoded.filter.expect("filter should round-trip");
        assert_eq!(f.bandwidth_hz, 12000);
        assert_eq!(f.sdr_gain_db, Some(18.0));
        assert_eq!(f.wfm_deemphasis_us, 50);
        assert!(f.wfm_stereo_detected);
        assert_eq!(f.wfm_cci, 12);
        assert_eq!(f.wfm_aci, 45);
        assert_eq!(f.sam_stereo_width, 0.5);
        assert!(!f.sam_carrier_sync);
    }

    fn minimal_snapshot() -> trx_core::rig::state::RigSnapshot {
        use trx_core::radio::freq::{Band, Freq};
        use trx_core::rig::state::{RigMode, RigSnapshot};
        use trx_core::rig::{RigAccessMethod, RigCapabilities, RigInfo, RigStatus};
        RigSnapshot {
            info: RigInfo {
                manufacturer: "Test".to_string(),
                model: "Mock".to_string(),
                revision: "1".to_string(),
                capabilities: RigCapabilities {
                    min_freq_step_hz: 1,
                    supported_bands: vec![Band {
                        low_hz: 14_000_000,
                        high_hz: 14_350_000,
                        tx_allowed: true,
                    }],
                    supported_modes: vec![RigMode::USB],
                    num_vfos: 1,
                    lock: false,
                    lockable: false,
                    attenuator: false,
                    preamp: false,
                    rit: false,
                    rpt: false,
                    split: false,
                    tx: false,
                    tx_limit: false,
                    vfo_switch: false,
                    filter_controls: true,
                    signal_meter: true,
                },
                access: RigAccessMethod::Tcp {
                    addr: "127.0.0.1:1234".to_string(),
                },
            },
            status: RigStatus {
                freq: Freq { hz: 14_074_000 },
                mode: RigMode::USB,
                tx_en: false,
                vfo: None,
                tx: None,
                rx: None,
                lock: None,
            },
            band: None,
            enabled: None,
            initialized: true,
            server_callsign: None,
            server_version: None,
            server_build_date: None,
            server_latitude: None,
            server_longitude: None,
            pskreporter_status: None,
            aprs_is_status: None,
            decoders: trx_core::DecoderConfig::default(),
            cw_auto: false,
            cw_wpm: 0,
            cw_tone_hz: 0,
            filter: None,
            spectrum: None,
            vchan_rds: None,
        }
    }
}
