use anyhow::{Context, Result};
use chainfire_client::Client;
use deployer_types::{ClusterNodeRecord, InstallState, PowerState};
use reqwest::{Client as HttpClient, Url};
use serde::Deserialize;
use serde_json::json;

fn cluster_prefix(cluster_namespace: &str, cluster_id: &str) -> String {
    format!("{}/clusters/{}/", cluster_namespace, cluster_id)
}

fn key_node(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec<u8> {
    format!(
        "{}nodes/{}",
        cluster_prefix(cluster_namespace, cluster_id),
        node_id
    )
    .into_bytes()
}

fn key_desired_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec<u8> {
    format!(
        "{}nodes/{}/desired-system",
        cluster_prefix(cluster_namespace, cluster_id),
        node_id
    )
    .into_bytes()
}

fn key_observed_system(cluster_namespace: &str, cluster_id: &str, node_id: &str) -> Vec<u8> {
    format!(
        "{}nodes/{}/observed-system",
        cluster_prefix(cluster_namespace, cluster_id),
        node_id
    )
    .into_bytes()
}

fn chainfire_endpoints(raw: &str) -> Vec<String> {
    raw.split(',')
        .map(str::trim)
        .filter(|endpoint| !endpoint.is_empty())
        .map(ToOwned::to_owned)
        .collect()
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PowerAction {
    On,
    Off,
    Cycle,
    Refresh,
}

impl PowerAction {
    fn parse(value: &str) -> Result<Self> {
        match value {
            "on" => Ok(Self::On),
            "off" => Ok(Self::Off),
            "cycle" => Ok(Self::Cycle),
            "refresh" => Ok(Self::Refresh),
            other => Err(anyhow::anyhow!("unsupported power action {}", other)),
        }
    }

    fn reset_type(self) -> Option<&'static str> {
        match self {
            Self::On => Some("On"),
            Self::Off => Some("ForceOff"),
            Self::Cycle => Some("PowerCycle"),
            Self::Refresh => None,
        }
    }
}

#[derive(Debug)]
struct RedfishTarget {
    resource_url: Url,
    username: Option<String>,
    password: Option<String>,
    insecure: bool,
}

#[derive(Debug, Deserialize)]
struct RedfishSystemView {
    #[serde(rename = "PowerState")]
    power_state: Option<String>,
}

impl RedfishTarget {
    fn parse(reference: &str) -> Result<Self> {
        let rewritten = if let Some(rest) = reference.strip_prefix("redfish+http://") {
            format!("http://{rest}")
        } else if let Some(rest) = reference.strip_prefix("redfish+https://") {
            format!("https://{rest}")
        } else if let Some(rest) = reference.strip_prefix("redfish://") {
            format!("https://{rest}")
        } else {
            return Err(anyhow::anyhow!(
                "unsupported BMC reference {}; expected redfish:// or redfish+http(s)://",
                reference
            ));
        };

        let mut resource_url = Url::parse(&rewritten)
            .with_context(|| format!("failed to parse BMC reference {}", reference))?;
        let insecure = resource_url
            .query_pairs()
            .any(|(key, value)| key == "insecure" && (value == "1" || value == "true"));
        let username = if resource_url.username().is_empty() {
            None
        } else {
            Some(resource_url.username().to_string())
        };
        let password = resource_url.password().map(ToOwned::to_owned);
        let system_path = normalize_redfish_system_path(resource_url.path());
        resource_url
            .set_username("")
            .map_err(|_| anyhow::anyhow!("failed to clear username from BMC reference"))?;
        resource_url
            .set_password(None)
            .map_err(|_| anyhow::anyhow!("failed to clear password from BMC reference"))?;
        resource_url.set_query(None);
        resource_url.set_path(&system_path);

        Ok(Self {
            resource_url,
            username,
            password,
            insecure,
        })
    }

    fn action_url(&self) -> Result<Url> {
        let mut action_url = self.resource_url.clone();
        let path = format!(
            "{}/Actions/ComputerSystem.Reset",
            self.resource_url.path().trim_end_matches('/')
        );
        action_url.set_path(&path);
        Ok(action_url)
    }

    async fn perform(&self, action: PowerAction) -> Result<PowerState> {
        let client = HttpClient::builder()
            .danger_accept_invalid_certs(self.insecure)
            .build()
            .context("failed to create Redfish client")?;

        if let Some(reset_type) = action.reset_type() {
            let request = self
                .with_auth(client.post(self.action_url()?))
                .json(&json!({ "ResetType": reset_type }));
            request
                .send()
                .await
                .context("failed to send Redfish reset request")?
                .error_for_status()
                .context("Redfish reset request failed")?;
        }

        match action {
            PowerAction::Cycle => Ok(PowerState::Cycling),
            PowerAction::On | PowerAction::Off | PowerAction::Refresh => {
                self.refresh(&client).await
            }
        }
    }

    async fn refresh(&self, client: &HttpClient) -> Result<PowerState> {
        let response = self
            .with_auth(client.get(self.resource_url.clone()))
            .send()
            .await
            .context("failed to query Redfish system resource")?
            .error_for_status()
            .context("Redfish system query failed")?;
        let system: RedfishSystemView = response
            .json()
            .await
            .context("failed to decode Redfish system response")?;
        map_redfish_power_state(system.power_state.as_deref())
    }

    fn with_auth(&self, request: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        match self.username.as_deref() {
            Some(username) => request.basic_auth(username, self.password.clone()),
            None => request,
        }
    }
}

fn normalize_redfish_system_path(path: &str) -> String {
    let trimmed = path.trim();
    if trimmed.is_empty() || trimmed == "/" {
        return "/redfish/v1/Systems/System.Embedded.1".to_string();
    }
    if trimmed.starts_with("/redfish/") {
        return trimmed.to_string();
    }
    format!("/redfish/v1/Systems/{}", trimmed.trim_start_matches('/'))
}

fn map_redfish_power_state(value: Option<&str>) -> Result<PowerState> {
    match value.unwrap_or("Unknown").to_ascii_lowercase().as_str() {
        "on" => Ok(PowerState::On),
        "off" => Ok(PowerState::Off),
        "poweringon" | "poweringoff" | "cycling" => Ok(PowerState::Cycling),
        "unknown" => Ok(PowerState::Unknown),
        other => Err(anyhow::anyhow!("unsupported Redfish power state {}", other)),
    }
}

async fn load_node_record(
    endpoint: &str,
    cluster_namespace: &str,
    cluster_id: &str,
    node_id: &str,
) -> Result<(Client, ClusterNodeRecord, Vec<u8>)> {
    let endpoints = chainfire_endpoints(endpoint);
    let mut last_error = None;

    for endpoint in endpoints {
        match Client::connect(endpoint.clone()).await {
            Ok(mut client) => {
                let key = key_node(cluster_namespace, cluster_id, node_id);
                let Some(bytes) = client.get(&key).await? else {
                    return Err(anyhow::anyhow!("node {} not found", node_id));
                };
                let node = serde_json::from_slice::<ClusterNodeRecord>(&bytes)
                    .context("failed to decode node record")?;
                return Ok((client, node, key));
            }
            Err(error) => last_error = Some(anyhow::Error::new(error)),
        }
    }

    Err(last_error.unwrap_or_else(|| anyhow::anyhow!("no Chainfire endpoints configured")))
}

pub async fn power_node(
    endpoint: &str,
    cluster_namespace: &str,
    cluster_id: &str,
    node_id: &str,
    action: &str,
) -> Result<()> {
    let action = PowerAction::parse(action)?;
    let (mut client, mut node, key) =
        load_node_record(endpoint, cluster_namespace, cluster_id, node_id).await?;
    let bmc_ref = node
        .bmc_ref
        .clone()
        .with_context(|| format!("node {} does not have a bmc_ref", node_id))?;
    let target = RedfishTarget::parse(&bmc_ref)?;
    let power_state = target.perform(action).await?;

    node.power_state = Some(power_state);
    client.put(&key, &serde_json::to_vec(&node)?).await?;
    println!("{}", serde_json::to_string_pretty(&node)?);
    Ok(())
}

pub async fn request_reinstall(
    endpoint: &str,
    cluster_namespace: &str,
    cluster_id: &str,
    node_id: &str,
    power_cycle: bool,
) -> Result<()> {
    let (mut client, mut node, key) =
        load_node_record(endpoint, cluster_namespace, cluster_id, node_id).await?;

    node.state = Some("provisioning".to_string());
    node.install_state = Some(InstallState::ReinstallRequested);

    if power_cycle {
        let bmc_ref = node
            .bmc_ref
            .clone()
            .with_context(|| format!("node {} does not have a bmc_ref", node_id))?;
        let target = RedfishTarget::parse(&bmc_ref)?;
        node.power_state = Some(target.perform(PowerAction::Cycle).await?);
    }

    client.put(&key, &serde_json::to_vec(&node)?).await?;
    client
        .delete(&key_desired_system(cluster_namespace, cluster_id, node_id))
        .await?;
    client
        .delete(&key_observed_system(cluster_namespace, cluster_id, node_id))
        .await?;
    println!("{}", serde_json::to_string_pretty(&node)?);
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        extract::State,
        http::StatusCode,
        routing::{get, post},
        Json, Router,
    };
    use serde_json::Value;
    use std::sync::{Arc, Mutex};
    use tokio::net::TcpListener;

    #[test]
    fn parse_redfish_short_reference_defaults_to_https() {
        let parsed = RedfishTarget::parse("redfish://lab-bmc/node01").unwrap();
        assert_eq!(
            parsed.resource_url.as_str(),
            "https://lab-bmc/redfish/v1/Systems/node01"
        );
    }

    #[test]
    fn parse_redfish_explicit_http_reference_keeps_query_flags_local() {
        let parsed =
            RedfishTarget::parse("redfish+http://user:pass@127.0.0.1/system-1?insecure=1").unwrap();
        assert_eq!(
            parsed.resource_url.as_str(),
            "http://127.0.0.1/redfish/v1/Systems/system-1"
        );
        assert_eq!(parsed.username.as_deref(), Some("user"));
        assert_eq!(parsed.password.as_deref(), Some("pass"));
        assert!(parsed.insecure);
    }

    #[tokio::test]
    async fn redfish_adapter_refreshes_and_resets_power() {
        #[derive(Clone, Default)]
        struct TestState {
            seen_payloads: Arc<Mutex<Vec<String>>>,
        }

        async fn system_handler() -> Json<Value> {
            Json(json!({ "PowerState": "On" }))
        }

        async fn reset_handler(
            State(state): State<TestState>,
            Json(payload): Json<Value>,
        ) -> StatusCode {
            state
                .seen_payloads
                .lock()
                .unwrap()
                .push(payload.to_string());
            StatusCode::NO_CONTENT
        }

        let state = TestState::default();
        let app = Router::new()
            .route("/redfish/v1/Systems/node01", get(system_handler))
            .route(
                "/redfish/v1/Systems/node01/Actions/ComputerSystem.Reset",
                post(reset_handler),
            )
            .with_state(state.clone());
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let addr = listener.local_addr().unwrap();
        let server = tokio::spawn(async move {
            axum::serve(listener, app).await.unwrap();
        });

        let target = RedfishTarget::parse(&format!(
            "redfish+http://{}/redfish/v1/Systems/node01",
            addr
        ))
        .unwrap();
        assert_eq!(
            target.perform(PowerAction::Refresh).await.unwrap(),
            PowerState::On
        );
        assert_eq!(
            target.perform(PowerAction::Off).await.unwrap(),
            PowerState::On
        );

        let payloads = state.seen_payloads.lock().unwrap().clone();
        assert_eq!(payloads, vec![r#"{"ResetType":"ForceOff"}"#.to_string()]);

        server.abort();
    }
}
