use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sha2::Sha256;
use std::collections::{BTreeMap, BTreeSet};
use url::Url;

type HmacSha256 = Hmac<Sha256>;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeAssetLocator {
    pub asset_id: String,
    pub workspace_id: Option<String>,
    pub document_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeSessionRef {
    pub session_id: String,
    pub asset_id: String,
    pub workspace_id: Option<String>,
    pub document_id: Option<String>,
    pub user_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeSignTokens {
    pub token: Option<String>,
    pub document_token: Option<String>,
    pub editor_config_token: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeHeader {
    pub name: String,
    pub value: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeProxyPreparationInput {
    pub encoded_url: String,
    pub method: String,
    pub range: Option<String>,
    pub supabase_url: Option<String>,
    pub supabase_internal_url: Option<String>,
    pub onlyoffice_storage_host_override: Option<String>,
    pub convex_origin: Option<String>,
    pub supabase_anon_key: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeProxyPreparation {
    pub target_url: String,
    pub forward_headers: Vec<OnlyOfficeHeader>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeCallbackPreparationInput {
    pub asset_id: String,
    pub document_id: Option<String>,
    pub workspace_id: Option<String>,
    pub user_id: Option<String>,
    pub session_id: Option<String>,
    pub status: i64,
    pub url: Option<String>,
    pub key: Option<String>,
    pub onlyoffice_internal_url: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeCallbackPreparation {
    pub should_write: bool,
    pub download_url: Option<String>,
    pub idempotency_key: Option<String>,
    pub locator: OnlyOfficeAssetLocator,
    pub session: OnlyOfficeSessionRef,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeForcesavePreparationInput {
    pub asset_id: String,
    pub key: String,
    pub onlyoffice_internal_url: String,
    pub secret: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeOutboundRequest {
    pub via: String,
    pub url: String,
    pub method: String,
    pub headers: Vec<OnlyOfficeHeader>,
    pub body_json: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeSessionResolveInput {
    pub asset_id: String,
    pub workspace_id: Option<String>,
    pub document_id: Option<String>,
    pub user_id: Option<String>,
    pub session_id: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnlyOfficeSessionResolveResult {
    pub locator: OnlyOfficeAssetLocator,
    pub session: OnlyOfficeSessionRef,
}

pub fn adapter_boundary_statement() -> &'static str {
    "adapter-onlyoffice 只定义资产定位、会话定位、签名/代理/callback/forcesave 边界，不把 OnlyOffice 变成主事实层。"
}

pub fn normalize_secret(raw: &str) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() {
        return String::new();
    }
    if ((trimmed.starts_with('"') && trimmed.ends_with('"'))
        || (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
        && trimmed.len() >= 2
    {
        return trimmed[1..trimmed.len() - 1].trim().to_string();
    }
    trimmed.to_string()
}

pub fn sign_config(config: &Value, secret: &str) -> Result<OnlyOfficeSignTokens, String> {
    let normalized_secret = normalize_secret(secret);
    if normalized_secret.is_empty() {
        return Ok(OnlyOfficeSignTokens {
            token: None,
            document_token: None,
            editor_config_token: None,
        });
    }

    let token = sign_hs256(config, &normalized_secret)?;
    let document_token = config
        .get("document")
        .map(|value| sign_hs256(value, &normalized_secret))
        .transpose()?;
    let editor_config_token = config
        .get("editorConfig")
        .map(|value| sign_hs256(value, &normalized_secret))
        .transpose()?;

    Ok(OnlyOfficeSignTokens {
        token: Some(token),
        document_token,
        editor_config_token,
    })
}

pub fn resolve_session(input: OnlyOfficeSessionResolveInput) -> OnlyOfficeSessionResolveResult {
    let session_id = input
        .session_id
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| format!("onlyoffice:{}", input.asset_id.trim()));
    let locator = OnlyOfficeAssetLocator {
        asset_id: input.asset_id.trim().to_string(),
        workspace_id: input.workspace_id.filter(|value| !value.trim().is_empty()),
        document_id: input.document_id.filter(|value| !value.trim().is_empty()),
    };
    let session = OnlyOfficeSessionRef {
        session_id,
        asset_id: locator.asset_id.clone(),
        workspace_id: locator.workspace_id.clone(),
        document_id: locator.document_id.clone(),
        user_id: input.user_id.filter(|value| !value.trim().is_empty()),
    };
    OnlyOfficeSessionResolveResult { locator, session }
}

pub fn prepare_proxy_request(
    input: OnlyOfficeProxyPreparationInput,
) -> Result<OnlyOfficeProxyPreparation, String> {
    let decoded = decode_base64url_to_utf8(&input.encoded_url)?;
    let mut target = Url::parse(&decoded).map_err(|_| "u 不是有效的 URL".to_string())?;

    if target.scheme() != "http" && target.scheme() != "https" {
        return Err("仅支持 http/https URL".into());
    }

    let supa = try_parse_origin_host(input.supabase_url.as_deref());
    let supa_internal_origin = try_parse_origin_url(input.supabase_internal_url.as_deref());
    let storage_override = try_parse_origin_host(input.onlyoffice_storage_host_override.as_deref());
    let convex_origin = try_parse_origin_url(input.convex_origin.as_deref());

    let is_supabase_path = target.path().starts_with("/storage/v1/")
        || target.path().starts_with("/auth/v1/")
        || target.path().starts_with("/rest/v1/")
        || target.path().starts_with("/functions/v1/")
        || target.path().starts_with("/realtime/v1/");

    if is_supabase_path {
        if let Some(origin) = supa_internal_origin.as_ref() {
            target.set_scheme(origin.scheme()).ok();
            target.set_host(origin.host_str()).ok();
            target.set_port(origin.port()).ok();
        } else if let Some(origin) = try_parse_origin_url(input.supabase_url.as_deref()) {
            target.set_scheme(origin.scheme()).ok();
            target.set_host(origin.host_str()).ok();
            target.set_port(origin.port()).ok();
        }
    }

    let mut allowed_hostnames = BTreeSet::new();
    let mut allowed_ports_by_hostname = BTreeMap::<String, BTreeSet<String>>::new();
    let mut add_allowed = |hostname: &str, port: &str| {
        if hostname.trim().is_empty() {
            return;
        }
        allowed_hostnames.insert(hostname.to_string());
        if !port.trim().is_empty() {
            allowed_ports_by_hostname
                .entry(hostname.to_string())
                .or_default()
                .insert(port.to_string());
        }
    };

    if let Some((hostname, port)) = supa.as_ref() {
        add_allowed(hostname, port);
        if is_local_hostname(hostname) {
            add_allowed("127.0.0.1", port);
            add_allowed("localhost", port);
            add_allowed("host.docker.internal", port);
        }
    }

    if let Some(origin) = supa_internal_origin.as_ref() {
        let port = origin
            .port()
            .map(|value| value.to_string())
            .unwrap_or_default();
        if let Some(hostname) = origin.host_str() {
            add_allowed(hostname, &port);
            if is_local_hostname(hostname) {
                add_allowed("127.0.0.1", &port);
                add_allowed("localhost", &port);
                add_allowed("host.docker.internal", &port);
            }
        }
    }

    if let Some((hostname, port)) = storage_override.as_ref() {
        add_allowed(hostname, port);
    }

    if let Some(origin) = convex_origin.as_ref() {
        let port = origin
            .port()
            .map(|value| value.to_string())
            .unwrap_or_default();
        if let Some(hostname) = origin.host_str() {
            add_allowed(hostname, &port);
            if is_local_hostname(hostname) {
                add_allowed("127.0.0.1", &port);
                add_allowed("localhost", &port);
                add_allowed("host.docker.internal", &port);
            }
        }
    }

    let hostname = target
        .host_str()
        .ok_or_else(|| "目标 URL 缺少 hostname".to_string())?;

    if is_private_ipv4(hostname)
        && !is_local_hostname(hostname)
        && !allowed_hostnames.contains(hostname)
    {
        return Err("禁止访问内网/私有地址".into());
    }

    if !allowed_hostnames.is_empty() && !allowed_hostnames.contains(hostname) {
        return Err("禁止代理到非允许的主机".into());
    }

    if let Some(allowed_ports) = allowed_ports_by_hostname.get(hostname) {
        if !allowed_ports.is_empty() {
            let port = target
                .port_or_known_default()
                .ok_or_else(|| "目标 URL 缺少端口信息".to_string())?
                .to_string();
            if !allowed_ports.contains(&port) {
                return Err("禁止代理到该端口".into());
            }
        }
    }

    let mut forward_headers = Vec::new();
    if let Some(range) = input.range.filter(|value| !value.trim().is_empty()) {
        forward_headers.push(OnlyOfficeHeader {
            name: "range".into(),
            value: range,
        });
    }

    if let Some(anon_key) = input
        .supabase_anon_key
        .filter(|value| !value.trim().is_empty())
    {
        let matches_public = supa
            .as_ref()
            .map(|(value, _)| value == hostname)
            .unwrap_or(false);
        let matches_internal = supa_internal_origin
            .as_ref()
            .and_then(|value| value.host_str())
            .map(|value| value == hostname)
            .unwrap_or(false);
        if matches_public || matches_internal {
            forward_headers.push(OnlyOfficeHeader {
                name: "apikey".into(),
                value: anon_key,
            });
        }
    }

    Ok(OnlyOfficeProxyPreparation {
        target_url: target.to_string(),
        forward_headers,
    })
}

pub fn prepare_callback(
    input: OnlyOfficeCallbackPreparationInput,
) -> Result<OnlyOfficeCallbackPreparation, String> {
    let session_result = resolve_session(OnlyOfficeSessionResolveInput {
        asset_id: input.asset_id.clone(),
        workspace_id: input.workspace_id.clone(),
        document_id: input.document_id.clone(),
        user_id: input.user_id.clone(),
        session_id: input
            .session_id
            .clone()
            .or_else(|| Some("onlyoffice-callback".into())),
    });

    if input.status != 2 && input.status != 6 {
        return Ok(OnlyOfficeCallbackPreparation {
            should_write: false,
            download_url: None,
            idempotency_key: input.key.filter(|value| !value.trim().is_empty()),
            locator: session_result.locator,
            session: session_result.session,
        });
    }

    let raw_url = input
        .url
        .filter(|value| !value.trim().is_empty())
        .ok_or_else(|| "OnlyOffice callback 缺少下载地址".to_string())?;

    Ok(OnlyOfficeCallbackPreparation {
        should_write: true,
        download_url: Some(rewrite_onlyoffice_download_url(
            &raw_url,
            &input.onlyoffice_internal_url,
        )),
        idempotency_key: input.key.filter(|value| !value.trim().is_empty()),
        locator: session_result.locator,
        session: session_result.session,
    })
}

pub fn prepare_forcesave(
    input: OnlyOfficeForcesavePreparationInput,
) -> Result<Vec<OnlyOfficeOutboundRequest>, String> {
    let base = input.onlyoffice_internal_url.trim().trim_end_matches('/');
    if base.is_empty() {
        return Err("缺少 onlyofficeInternalUrl".into());
    }
    if input.asset_id.trim().is_empty() {
        return Err("缺少 assetId".into());
    }
    if input.key.trim().is_empty() {
        return Err("缺少 key".into());
    }

    let payload = json!({
        "c": "forcesave",
        "key": input.key,
        "userdata": format!("asset:{}", input.asset_id.trim()),
    });
    let raw_body_json = serde_json::to_string(&payload).map_err(|error| error.to_string())?;

    let normalized_secret = normalize_secret(input.secret.as_deref().unwrap_or(""));
    let mut requests = Vec::new();

    if !normalized_secret.is_empty() {
        let token = sign_hs256(&payload, &normalized_secret)?;
        requests.push(OnlyOfficeOutboundRequest {
            via: "command".into(),
            url: format!("{base}/command"),
            method: "POST".into(),
            headers: vec![OnlyOfficeHeader {
                name: "content-type".into(),
                value: "application/json".into(),
            }],
            body_json: serde_json::to_string(&json!({ "token": token }))
                .map_err(|error| error.to_string())?,
        });
        requests.push(OnlyOfficeOutboundRequest {
            via: "forcesave".into(),
            url: format!("{base}/forcesave"),
            method: "POST".into(),
            headers: vec![OnlyOfficeHeader {
                name: "content-type".into(),
                value: "application/json".into(),
            }],
            body_json: raw_body_json,
        });
        return Ok(requests);
    }

    requests.push(OnlyOfficeOutboundRequest {
        via: "forcesave".into(),
        url: format!("{base}/forcesave"),
        method: "POST".into(),
        headers: vec![OnlyOfficeHeader {
            name: "content-type".into(),
            value: "application/json".into(),
        }],
        body_json: raw_body_json.clone(),
    });
    requests.push(OnlyOfficeOutboundRequest {
        via: "command_no_token".into(),
        url: format!("{base}/command"),
        method: "POST".into(),
        headers: vec![OnlyOfficeHeader {
            name: "content-type".into(),
            value: "application/json".into(),
        }],
        body_json: raw_body_json,
    });
    Ok(requests)
}

pub fn rewrite_onlyoffice_download_url(raw: &str, onlyoffice_internal_url: &str) -> String {
    let base = onlyoffice_internal_url.trim().trim_end_matches('/');
    if base.is_empty() {
        return raw.to_string();
    }
    let Ok(url) = Url::parse(raw) else {
        return raw.to_string();
    };
    let prefix = "/onlyoffice-server";
    if !url.path().starts_with(prefix) {
        return raw.to_string();
    }
    let next_path = url.path()[prefix.len()..].trim_start_matches('/');
    if next_path.is_empty() {
        return raw.to_string();
    }
    let mut rewritten = format!("{base}/{next_path}");
    if let Some(query) = url.query() {
        rewritten.push('?');
        rewritten.push_str(query);
    }
    rewritten
}

fn sign_hs256(payload: &Value, secret: &str) -> Result<String, String> {
    let header = json!({ "alg": "HS256", "typ": "JWT" });
    let header_part = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
    let payload_part = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
    let signing_input = format!("{header_part}.{payload_part}");
    let mut mac =
        HmacSha256::new_from_slice(secret.as_bytes()).map_err(|error| error.to_string())?;
    mac.update(signing_input.as_bytes());
    let signature = mac.finalize().into_bytes();
    let signature_part = URL_SAFE_NO_PAD.encode(signature);
    Ok(format!("{signing_input}.{signature_part}"))
}

fn decode_base64url_to_utf8(input: &str) -> Result<String, String> {
    let bytes = URL_SAFE_NO_PAD
        .decode(input.trim())
        .map_err(|_| "u 不是有效的 base64url URL".to_string())?;
    String::from_utf8(bytes).map_err(|_| "u 不是有效的 base64url URL".to_string())
}

fn is_local_hostname(hostname: &str) -> bool {
    matches!(hostname, "127.0.0.1" | "localhost" | "host.docker.internal")
}

fn is_private_ipv4(hostname: &str) -> bool {
    let parts = hostname
        .split('.')
        .map(str::parse::<u16>)
        .collect::<Result<Vec<_>, _>>();
    let Ok(parts) = parts else {
        return false;
    };
    if parts.len() != 4 || parts.iter().any(|value| *value > 255) {
        return false;
    }
    let a = parts[0];
    let b = parts[1];
    a == 10
        || a == 127
        || (a == 169 && b == 254)
        || (a == 172 && (16..=31).contains(&b))
        || (a == 192 && b == 168)
        || a == 0
}

fn try_parse_origin_host(raw: Option<&str>) -> Option<(String, String)> {
    let value = raw?.trim();
    if value.is_empty() {
        return None;
    }
    if value.starts_with("http://") || value.starts_with("https://") {
        let url = Url::parse(value).ok()?;
        return Some((
            url.host_str()?.to_string(),
            url.port()
                .map(|value| value.to_string())
                .unwrap_or_default(),
        ));
    }
    Some((value.to_string(), String::new()))
}

fn try_parse_origin_url(raw: Option<&str>) -> Option<Url> {
    let value = raw?.trim();
    if value.is_empty() {
        return None;
    }
    Url::parse(value)
        .or_else(|_| Url::parse(&format!("http://{value}")))
        .ok()
}

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

    #[test]
    fn boundary_statement_mentions_non_source_of_truth() {
        assert!(adapter_boundary_statement().contains("不把 OnlyOffice 变成主事实层"));
    }

    #[test]
    fn normalize_secret_strips_quotes() {
        assert_eq!(normalize_secret(" \"abc\" "), "abc");
        assert_eq!(normalize_secret("'xyz'"), "xyz");
    }

    #[test]
    fn sign_config_returns_document_and_editor_tokens() {
        let tokens = sign_config(
            &json!({
                "document": {"title": "A"},
                "editorConfig": {"mode": "edit"},
            }),
            "secret",
        )
        .expect("sign should succeed");
        assert!(tokens.token.is_some());
        assert!(tokens.document_token.is_some());
        assert!(tokens.editor_config_token.is_some());
    }

    #[test]
    fn proxy_request_rewrites_supabase_path_to_internal_origin() {
        let encoded_url = URL_SAFE_NO_PAD.encode(
            b"https://public.example.com/storage/v1/object/sign/documents/a.docx?token=abc",
        );
        let result = prepare_proxy_request(OnlyOfficeProxyPreparationInput {
            encoded_url,
            method: "GET".into(),
            range: Some("bytes=0-99".into()),
            supabase_url: Some("https://public.example.com".into()),
            supabase_internal_url: Some("http://127.0.0.1:18000".into()),
            onlyoffice_storage_host_override: None,
            convex_origin: Some("http://127.0.0.1:3210".into()),
            supabase_anon_key: Some("anon".into()),
        })
        .expect("proxy should prepare");
        assert!(result
            .target_url
            .starts_with("http://127.0.0.1:18000/storage/v1/"));
        assert_eq!(result.forward_headers.len(), 2);
    }

    #[test]
    fn callback_preparation_rewrites_onlyoffice_server_path() {
        let result = prepare_callback(OnlyOfficeCallbackPreparationInput {
            asset_id: "asset_1".into(),
            document_id: Some("doc_1".into()),
            workspace_id: Some("ws_1".into()),
            user_id: Some("user_1".into()),
            session_id: None,
            status: 6,
            url: Some(
                "http://app.example.com/onlyoffice-server/cache/files/out.docx?token=1".into(),
            ),
            key: Some("doc_key_1".into()),
            onlyoffice_internal_url: "http://127.0.0.1:8082".into(),
        })
        .expect("callback should prepare");
        assert!(result.should_write);
        assert_eq!(
            result.download_url.as_deref(),
            Some("http://127.0.0.1:8082/cache/files/out.docx?token=1")
        );
        assert_eq!(result.session.session_id, "onlyoffice-callback");
    }

    #[test]
    fn forcesave_preparation_prefers_command_when_secret_present() {
        let result = prepare_forcesave(OnlyOfficeForcesavePreparationInput {
            asset_id: "asset_1".into(),
            key: "doc_key_1".into(),
            onlyoffice_internal_url: "http://127.0.0.1:8082".into(),
            secret: Some("secret".into()),
        })
        .expect("forcesave should prepare");
        assert_eq!(result[0].via, "command");
        assert_eq!(result[1].via, "forcesave");
    }
}
