use crate::introspect::cache::{
    compute_cache_key, CacheLookupStatus, DeviceFingerprint, DEFAULT_REMOTE_SCHEMA_TTL_SECONDS,
};
use crate::introspect::CommandDefinition;
use crate::{args::ParsedInvocation, error::ErrorCode};
use serde::Serialize;
use std::collections::BTreeMap;

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RemoteOverlayCommand {
    pub name: String,
    pub support: String,
    pub output_fields_observed: Vec<String>,
    pub runtime_value_hints: BTreeMap<String, RuntimeValueHint>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attempted_side_effects_override: Option<Vec<String>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attempted_idempotency_override: Option<String>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RuntimeValueHint {
    pub values: Vec<String>,
    pub source: String,
    pub completeness: String,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct StaticCommandPolicy {
    pub name: String,
    pub side_effects: Vec<String>,
    pub idempotency: String,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct MergedCommand {
    pub name: String,
    pub support: String,
    pub schema_source: Vec<String>,
    pub side_effects: Vec<String>,
    pub idempotency: String,
    pub output_fields_observed: Vec<String>,
    pub runtime_value_hints: BTreeMap<String, RuntimeValueHint>,
    pub warnings: Vec<String>,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RemoteSchemaCacheStatus {
    pub status: String,
    pub ttl_seconds: u64,
    pub cache_key: String,
}

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
pub struct RemoteSchemaSnapshot {
    pub schema_version: String,
    pub schema_source: Vec<String>,
    pub profile: String,
    pub device: DeviceFingerprint,
    pub cache: RemoteSchemaCacheStatus,
    pub commands: Vec<MergedCommand>,
    pub warnings: Vec<String>,
}

pub fn merge_overlay(
    policy: &StaticCommandPolicy,
    overlay: &RemoteOverlayCommand,
) -> MergedCommand {
    MergedCommand {
        name: policy.name.clone(),
        support: overlay.support.clone(),
        schema_source: vec!["static_catalog".to_owned(), "remote_overlay".to_owned()],
        side_effects: policy.side_effects.clone(),
        idempotency: policy.idempotency.clone(),
        output_fields_observed: overlay.output_fields_observed.clone(),
        runtime_value_hints: overlay.runtime_value_hints.clone(),
        warnings: overlay.warnings.clone(),
    }
}

pub fn remote_schema_unavailable_snapshot(
    profile: &str,
    fingerprint: &DeviceFingerprint,
) -> RemoteSchemaSnapshot {
    RemoteSchemaSnapshot {
        schema_version: "roswire.remote.schema.v1".to_owned(),
        schema_source: vec!["static_catalog".to_owned(), "remote_overlay".to_owned()],
        profile: profile.to_owned(),
        device: fingerprint.clone(),
        cache: RemoteSchemaCacheStatus {
            status: "unavailable".to_owned(),
            ttl_seconds: DEFAULT_REMOTE_SCHEMA_TTL_SECONDS,
            cache_key: compute_cache_key(profile, fingerprint),
        },
        commands: Vec::new(),
        warnings: vec!["REMOTE_SCHEMA_UNAVAILABLE".to_owned()],
    }
}

pub fn degraded_remote_schema_snapshot(
    profile: &str,
    fingerprint: &DeviceFingerprint,
    policies: Vec<StaticCommandPolicy>,
    warning: impl Into<String>,
) -> RemoteSchemaSnapshot {
    degraded_remote_schema_snapshot_with_cache_status(
        profile,
        fingerprint,
        policies,
        warning,
        CacheLookupStatus::Miss,
    )
}

pub fn degraded_remote_schema_snapshot_with_cache_status(
    profile: &str,
    fingerprint: &DeviceFingerprint,
    policies: Vec<StaticCommandPolicy>,
    warning: impl Into<String>,
    cache_status: CacheLookupStatus,
) -> RemoteSchemaSnapshot {
    let warning = warning.into();
    let commands = policies
        .into_iter()
        .map(|policy| {
            let overlay = RemoteOverlayCommand {
                name: policy.name.clone(),
                support: "unknown".to_owned(),
                output_fields_observed: static_output_fields(&policy.name),
                runtime_value_hints: static_runtime_value_hints(&policy.name),
                attempted_side_effects_override: None,
                attempted_idempotency_override: None,
                warnings: vec![warning.clone()],
            };
            merge_overlay(&policy, &overlay)
        })
        .collect();

    RemoteSchemaSnapshot {
        schema_version: "roswire.remote.schema.v1".to_owned(),
        schema_source: vec!["static_catalog".to_owned(), "remote_overlay".to_owned()],
        profile: profile.to_owned(),
        device: fingerprint.clone(),
        cache: RemoteSchemaCacheStatus {
            status: cache_status.as_str().to_owned(),
            ttl_seconds: DEFAULT_REMOTE_SCHEMA_TTL_SECONDS,
            cache_key: compute_cache_key(profile, fingerprint),
        },
        commands,
        warnings: vec![warning],
    }
}

fn static_runtime_value_hints(command: &str) -> BTreeMap<String, RuntimeValueHint> {
    let hints: &[(&str, &[&str])] = match command {
        "ip firewall filter print" => &[
            ("chain", &["input", "forward", "output"]),
            ("action", &["accept", "drop", "reject"]),
        ],
        "ip firewall nat print" => &[
            ("chain", &["srcnat", "dstnat"]),
            ("action", &["masquerade", "dst-nat", "src-nat"]),
        ],
        "ip route print" => &[("routing-table", &["main"])],
        "tool netwatch print" => &[("status", &["up", "down", "unknown"])],
        "user print" => &[("group", &["full", "read", "write"])],
        _ => &[],
    };

    hints
        .iter()
        .map(|(field, values)| {
            (
                (*field).to_owned(),
                RuntimeValueHint {
                    values: values.iter().map(|value| (*value).to_owned()).collect(),
                    source: "static_catalog_hint".to_owned(),
                    completeness: "not_exhaustive".to_owned(),
                },
            )
        })
        .collect()
}

pub fn policy_from_command(command: &CommandDefinition) -> Option<StaticCommandPolicy> {
    let tokens = command.name.split_whitespace().collect::<Vec<_>>();
    let action = tokens.last()?;
    let path = tokens[..tokens.len().saturating_sub(1)]
        .iter()
        .map(|token| (*token).to_owned())
        .collect::<Vec<_>>();
    let invocation = ParsedInvocation {
        path,
        action: (*action).to_owned(),
        resolved_args: BTreeMap::new(),
        flags: Vec::new(),
    };
    let mapping = crate::mapping::resolve_mapping(&invocation).ok()?;

    Some(StaticCommandPolicy {
        name: command.name.clone(),
        side_effects: mapping.side_effects,
        idempotency: mapping.idempotency,
    })
}

pub fn policies_from_catalog(commands: &[CommandDefinition]) -> Vec<StaticCommandPolicy> {
    commands.iter().filter_map(policy_from_command).collect()
}

pub fn unknown_fingerprint(host: &str, selected_protocol: &str) -> DeviceFingerprint {
    DeviceFingerprint {
        host_id_hashed: crate::introspect::cache::hash_host_id(host),
        routeros_version: "unknown".to_owned(),
        build_time: "unknown".to_owned(),
        architecture: "unknown".to_owned(),
        board_name: "unknown".to_owned(),
        packages_hash: "unknown".to_owned(),
        selected_protocol: selected_protocol.to_owned(),
    }
}

pub fn warning_name(code: ErrorCode) -> String {
    serde_json::to_value(code)
        .ok()
        .and_then(|value| value.as_str().map(str::to_owned))
        .unwrap_or_else(|| "CAPABILITY_PROBE_FAILED".to_owned())
}

fn static_output_fields(command: &str) -> Vec<String> {
    match command {
        "system resource print" => vec![
            "version".to_owned(),
            "architecture-name".to_owned(),
            "board-name".to_owned(),
        ],
        "system package print" => vec![
            ".id".to_owned(),
            "name".to_owned(),
            "version".to_owned(),
            "build-time".to_owned(),
            "disabled".to_owned(),
        ],
        "tool mac-server print" => vec![
            ".id".to_owned(),
            "allowed-interface-list".to_owned(),
            "disabled".to_owned(),
        ],
        "tool netwatch print" => vec![
            ".id".to_owned(),
            "host".to_owned(),
            "type".to_owned(),
            "interval".to_owned(),
            "timeout".to_owned(),
            "status".to_owned(),
            "disabled".to_owned(),
            "comment".to_owned(),
        ],
        "user print" => vec![
            ".id".to_owned(),
            "name".to_owned(),
            "group".to_owned(),
            "address".to_owned(),
            "disabled".to_owned(),
            "last-logged-in".to_owned(),
        ],
        "interface print" => vec![".id".to_owned(), "name".to_owned(), "disabled".to_owned()],
        "interface wireguard print" => vec![
            ".id".to_owned(),
            "name".to_owned(),
            "listen-port".to_owned(),
            "mtu".to_owned(),
            "running".to_owned(),
            "disabled".to_owned(),
        ],
        "interface wireguard peers print" => vec![
            ".id".to_owned(),
            "interface".to_owned(),
            "public-key".to_owned(),
            "endpoint-address".to_owned(),
            "endpoint-port".to_owned(),
            "allowed-address".to_owned(),
            "disabled".to_owned(),
            "comment".to_owned(),
        ],
        "ip address print" => vec![
            ".id".to_owned(),
            "address".to_owned(),
            "network".to_owned(),
            "interface".to_owned(),
            "disabled".to_owned(),
        ],
        "ip dhcp-client print" => vec![
            ".id".to_owned(),
            "interface".to_owned(),
            "address".to_owned(),
            "gateway".to_owned(),
            "status".to_owned(),
            "disabled".to_owned(),
        ],
        "ip firewall address-list print" => vec![
            ".id".to_owned(),
            "list".to_owned(),
            "address".to_owned(),
            "timeout".to_owned(),
            "dynamic".to_owned(),
            "disabled".to_owned(),
            "comment".to_owned(),
        ],
        "ip firewall filter print" => vec![
            ".id".to_owned(),
            "chain".to_owned(),
            "action".to_owned(),
            "src-address".to_owned(),
            "dst-address".to_owned(),
            "protocol".to_owned(),
            "disabled".to_owned(),
            "comment".to_owned(),
        ],
        "ip firewall nat print" => vec![
            ".id".to_owned(),
            "chain".to_owned(),
            "action".to_owned(),
            "src-address".to_owned(),
            "dst-address".to_owned(),
            "to-addresses".to_owned(),
            "to-ports".to_owned(),
            "disabled".to_owned(),
            "comment".to_owned(),
        ],
        "ip firewall connection print" => vec![
            ".id".to_owned(),
            "src-address".to_owned(),
            "dst-address".to_owned(),
            "protocol".to_owned(),
            "tcp-state".to_owned(),
            "timeout".to_owned(),
        ],
        "ip route print" => vec![
            ".id".to_owned(),
            "dst-address".to_owned(),
            "gateway".to_owned(),
            "distance".to_owned(),
            "routing-table".to_owned(),
            "pref-src".to_owned(),
            "active".to_owned(),
            "dynamic".to_owned(),
            "disabled".to_owned(),
        ],
        _ => Vec::new(),
    }
}

#[cfg(test)]
mod tests {
    use super::{
        degraded_remote_schema_snapshot, degraded_remote_schema_snapshot_with_cache_status,
        merge_overlay, policies_from_catalog, remote_schema_unavailable_snapshot,
        unknown_fingerprint, warning_name, RemoteOverlayCommand, RuntimeValueHint,
        StaticCommandPolicy,
    };
    use crate::error::ErrorCode;
    use crate::introspect::cache::{hash_host_id, CacheLookupStatus, DeviceFingerprint};
    use crate::introspect::CommandDefinition;
    use std::collections::BTreeMap;

    fn fingerprint() -> DeviceFingerprint {
        DeviceFingerprint {
            host_id_hashed: hash_host_id("192.168.88.1"),
            routeros_version: "7.15.3".to_owned(),
            build_time: "2026-01-01".to_owned(),
            architecture: "arm64".to_owned(),
            board_name: "RB5009".to_owned(),
            packages_hash: "pkg-hash".to_owned(),
            selected_protocol: "rest".to_owned(),
        }
    }

    #[test]
    fn merge_keeps_static_safety_fields() {
        let policy = StaticCommandPolicy {
            name: "ip address add".to_owned(),
            side_effects: vec!["creates-routeros-record".to_owned()],
            idempotency: "not-idempotent".to_owned(),
        };
        let overlay = RemoteOverlayCommand {
            name: "ip address add".to_owned(),
            support: "supported".to_owned(),
            output_fields_observed: vec![".id".to_owned(), "address".to_owned()],
            runtime_value_hints: BTreeMap::from([(
                "interface".to_owned(),
                RuntimeValueHint {
                    values: vec!["bridge".to_owned(), "ether1".to_owned()],
                    source: "remote_observed".to_owned(),
                    completeness: "not_exhaustive".to_owned(),
                },
            )]),
            attempted_side_effects_override: Some(vec!["none".to_owned()]),
            attempted_idempotency_override: Some("idempotent".to_owned()),
            warnings: Vec::new(),
        };

        let merged = merge_overlay(&policy, &overlay);

        assert_eq!(merged.side_effects, vec!["creates-routeros-record"]);
        assert_eq!(merged.idempotency, "not-idempotent");
        assert_eq!(merged.support, "supported");
        assert_eq!(
            merged
                .runtime_value_hints
                .get("interface")
                .map(|hint| hint.source.as_str()),
            Some("remote_observed")
        );
    }

    #[test]
    fn unavailable_snapshot_has_warning_and_hashed_cache_key() {
        let fp = fingerprint();
        let snapshot = remote_schema_unavailable_snapshot("home", &fp);

        assert_eq!(snapshot.schema_version, "roswire.remote.schema.v1");
        assert!(snapshot
            .warnings
            .iter()
            .any(|w| w == "REMOTE_SCHEMA_UNAVAILABLE"));
        assert!(snapshot.cache.cache_key.starts_with("cache:"));
        assert!(!snapshot.cache.cache_key.contains("192.168.88.1"));
    }

    #[test]
    fn degraded_snapshot_keeps_static_policy_and_uses_hashed_cache_key() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![StaticCommandPolicy {
            name: "ip address add".to_owned(),
            side_effects: vec!["creates-routeros-record".to_owned()],
            idempotency: "not-idempotent".to_owned(),
        }];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::NetworkError),
        );

        assert_eq!(snapshot.cache.status, "miss");
        assert_eq!(snapshot.cache.ttl_seconds, 604_800);
        assert!(!snapshot.cache.cache_key.contains("198.51.100.10"));
        assert_eq!(snapshot.commands[0].support, "unknown");
        assert_eq!(
            snapshot.commands[0].side_effects,
            vec!["creates-routeros-record"]
        );
        assert_eq!(snapshot.commands[0].idempotency, "not-idempotent");
        assert!(snapshot.warnings.iter().any(|item| item == "NETWORK_ERROR"));
    }

    #[test]
    fn degraded_snapshot_can_report_refresh_cache_status() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![StaticCommandPolicy {
            name: "ip address print".to_owned(),
            side_effects: Vec::new(),
            idempotency: "read-only".to_owned(),
        }];

        let snapshot = degraded_remote_schema_snapshot_with_cache_status(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
            CacheLookupStatus::Refresh,
        );

        assert_eq!(snapshot.cache.status, "refresh");
        assert!(snapshot.warnings.iter().any(|item| item == "CONFIG_ERROR"));
    }

    #[test]
    fn degraded_snapshot_does_not_include_plain_host_or_username() {
        let fp = unknown_fingerprint("203.0.113.42", "rest");
        let policies = vec![StaticCommandPolicy {
            name: "ip address print".to_owned(),
            side_effects: Vec::new(),
            idempotency: "read-only".to_owned(),
        }];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::NetworkError),
        );
        let json = serde_json::to_string(&snapshot).expect("snapshot should serialize");

        assert!(!json.contains("203.0.113.42"));
        assert!(!json.contains("admin"));
        assert!(json.contains(&fp.host_id_hashed));
    }

    #[test]
    fn policies_from_catalog_filters_to_routeros_mapped_commands() {
        let commands = vec![
            CommandDefinition {
                name: "ip address print".to_owned(),
                summary: String::new(),
                kind: "routeros-command".to_owned(),
                syntax: String::new(),
                arguments: Vec::new(),
                examples: Vec::new(),
                errors: Vec::new(),
            },
            CommandDefinition {
                name: "config inspect".to_owned(),
                summary: String::new(),
                kind: "config".to_owned(),
                syntax: String::new(),
                arguments: Vec::new(),
                examples: Vec::new(),
                errors: Vec::new(),
            },
        ];

        let policies = policies_from_catalog(&commands);

        assert_eq!(policies.len(), 1);
        assert_eq!(policies[0].name, "ip address print");
        assert_eq!(policies[0].idempotency, "read-only");
    }

    #[test]
    fn degraded_snapshot_includes_system_package_static_fields() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![StaticCommandPolicy {
            name: "system package print".to_owned(),
            side_effects: Vec::new(),
            idempotency: "read-only".to_owned(),
        }];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
        );

        assert_eq!(snapshot.commands[0].name, "system package print");
        assert_eq!(snapshot.commands[0].idempotency, "read-only");
        assert_eq!(
            snapshot.commands[0].output_fields_observed,
            vec![".id", "name", "version", "build-time", "disabled"]
        );
    }

    #[test]
    fn degraded_snapshot_includes_user_static_fields() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![StaticCommandPolicy {
            name: "user print".to_owned(),
            side_effects: Vec::new(),
            idempotency: "read-only".to_owned(),
        }];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
        );

        assert_eq!(snapshot.commands[0].name, "user print");
        assert_eq!(snapshot.commands[0].idempotency, "read-only");
        assert_eq!(
            snapshot.commands[0].output_fields_observed,
            vec![
                ".id",
                "name",
                "group",
                "address",
                "disabled",
                "last-logged-in"
            ]
        );
    }

    #[test]
    fn degraded_snapshot_includes_ip_route_static_fields() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![StaticCommandPolicy {
            name: "ip route print".to_owned(),
            side_effects: Vec::new(),
            idempotency: "read-only".to_owned(),
        }];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
        );

        assert_eq!(snapshot.commands[0].name, "ip route print");
        assert_eq!(snapshot.commands[0].idempotency, "read-only");
        assert_eq!(
            snapshot.commands[0].output_fields_observed,
            vec![
                ".id",
                "dst-address",
                "gateway",
                "distance",
                "routing-table",
                "pref-src",
                "active",
                "dynamic",
                "disabled"
            ]
        );
    }

    #[test]
    fn degraded_snapshot_includes_firewall_static_fields() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![
            StaticCommandPolicy {
                name: "ip firewall address-list print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
            StaticCommandPolicy {
                name: "ip firewall filter print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
            StaticCommandPolicy {
                name: "ip firewall nat print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
        ];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
        );

        assert_eq!(snapshot.commands[0].name, "ip firewall address-list print");
        assert!(snapshot.commands[0]
            .output_fields_observed
            .contains(&"list".to_owned()));
        assert_eq!(snapshot.commands[1].name, "ip firewall filter print");
        assert!(snapshot.commands[1]
            .output_fields_observed
            .contains(&"chain".to_owned()));
        assert_eq!(
            snapshot.commands[1]
                .runtime_value_hints
                .get("chain")
                .map(|hint| hint.completeness.as_str()),
            Some("not_exhaustive")
        );
        assert_eq!(snapshot.commands[2].name, "ip firewall nat print");
        assert!(snapshot.commands[2]
            .output_fields_observed
            .contains(&"to-addresses".to_owned()));
    }

    #[test]
    fn degraded_snapshot_includes_tool_static_fields() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![
            StaticCommandPolicy {
                name: "tool mac-server print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
            StaticCommandPolicy {
                name: "tool netwatch print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
        ];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
        );

        assert_eq!(snapshot.commands[0].name, "tool mac-server print");
        assert!(snapshot.commands[0]
            .output_fields_observed
            .contains(&"allowed-interface-list".to_owned()));
        assert_eq!(snapshot.commands[1].name, "tool netwatch print");
        assert!(snapshot.commands[1]
            .output_fields_observed
            .contains(&"status".to_owned()));
    }

    #[test]
    fn degraded_snapshot_includes_wireguard_static_fields_without_private_material() {
        let fp = unknown_fingerprint("198.51.100.10", "unknown");
        let policies = vec![
            StaticCommandPolicy {
                name: "interface wireguard print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
            StaticCommandPolicy {
                name: "interface wireguard peers print".to_owned(),
                side_effects: Vec::new(),
                idempotency: "read-only".to_owned(),
            },
        ];

        let snapshot = degraded_remote_schema_snapshot(
            "studio",
            &fp,
            policies,
            warning_name(ErrorCode::ConfigError),
        );

        assert_eq!(snapshot.commands[0].name, "interface wireguard print");
        assert!(snapshot.commands[0]
            .output_fields_observed
            .iter()
            .all(|field| !field.contains("private")));
        assert_eq!(
            snapshot.commands[1].output_fields_observed,
            vec![
                ".id",
                "interface",
                "public-key",
                "endpoint-address",
                "endpoint-port",
                "allowed-address",
                "disabled",
                "comment"
            ]
        );
        assert!(snapshot.commands[1]
            .output_fields_observed
            .iter()
            .all(|field| !field.contains("preshared")));
    }
}
