Documentation
// Copyright (c) 2026, Salesforce, Inc.,
// All rights reserved.
// For full license text, see the LICENSE.txt file

use crate::constants::{
    DESTINATION_ADDRESS, METHOD_HEADER, PATH_HEADER, REQUEST_PROTOCOL, REQUEST_SCHEME,
    SCHEME_HEADER, SOURCE_ADDRESS, STATUS_CODE_HEADER,
};
use classy::event::HeadersAccessor;
use classy::hl::{HeadersHandler, HttpClientResponse, PropertyAccessor};
use pel::runtime::value::Value as PelValue;
use std::collections::HashMap;
use url::Url;

/// Binding for the top-level `attributes` variable.
pub trait AttributesBinding {
    /// Returns the entire headers map.
    fn extract_headers(&self) -> HashMap<String, String>;

    /// Returns a single header.
    fn extract_header(&self, key: &str) -> Option<String>;

    /// Returns a map with the query parameters.
    fn extract_query_params(&self) -> HashMap<String, String> {
        self.extract_header(PATH_HEADER)
            .and_then(fake_url)
            .map(|url| {
                url.query_pairs()
                    .map(|(k, v)| (k.to_string(), v.to_string()))
                    .collect()
            })
            .unwrap_or_default()
    }

    /// Returns the `attributes.method` value.
    fn method(&self) -> Option<String> {
        self.extract_header(METHOD_HEADER)
    }

    /// Returns the `attributes.path` value.
    fn path(&self) -> Option<String> {
        let mut url = self.extract_header(PATH_HEADER).and_then(fake_url)?;
        url.set_query(None);
        Some(url.path().to_string())
    }

    /// Returns the `uri` value.
    fn uri(&self) -> Option<String> {
        self.extract_header(PATH_HEADER)
    }

    /// Returns the `attributes.remoteAddress` value.
    fn remote_address(&self) -> Option<String> {
        None
    }

    /// Returns the `attributes.localAddress` value.
    fn local_address(&self) -> Option<String> {
        None
    }

    /// Returns the `attributes.queryString` value.
    fn query_string(&self) -> Option<String> {
        let path = self.extract_header(PATH_HEADER)?;
        fake_url(path)?.query().map(str::to_string)
    }

    /// Returns the `attributes.scheme` value.
    fn scheme(&self) -> Option<String> {
        self.extract_header(SCHEME_HEADER)
    }

    /// Returns the `attributes.version` value.
    fn version(&self) -> Option<String> {
        None
    }

    /// Returns the `attributes.statusCode` value.
    fn status_code(&self) -> Option<u32> {
        self.extract_header(STATUS_CODE_HEADER)
            .and_then(|value| value.parse::<u32>().ok())
    }
}

impl AttributesBinding for HttpClientResponse {
    fn extract_headers(&self) -> HashMap<String, String> {
        self.headers().clone()
    }

    fn extract_header(&self, key: &str) -> Option<String> {
        self.header(key).cloned()
    }
}

/// A wrapper that implements the [`AttributesBinding`] trait for the handlers provided by the PDK.
pub struct HandlerAttributesBinding<'a> {
    handler: &'a dyn HeadersHandler,
    properties: Option<&'a dyn PropertyAccessor>,
}

impl<'a> HandlerAttributesBinding<'a> {
    /// Will create a binding that will resolve all properties of the attributes variable.
    pub fn new(handler: &'a dyn HeadersHandler, properties: &'a dyn PropertyAccessor) -> Self {
        Self {
            handler,
            properties: Some(properties),
        }
    }

    /// Will create a binding that won't be able to resolve remote_address, local_address, scheme or version
    pub fn partial(handler: &'a dyn HeadersHandler) -> Self {
        Self {
            handler,
            properties: None,
        }
    }
}

impl AttributesBinding for HandlerAttributesBinding<'_> {
    fn extract_headers(&self) -> HashMap<String, String> {
        self.handler.headers().into_iter().collect()
    }

    fn extract_header(&self, key: &str) -> Option<String> {
        self.handler.header(key)
    }

    fn remote_address(&self) -> Option<String> {
        self.properties.and_then(remote_address)
    }

    fn local_address(&self) -> Option<String> {
        self.properties.and_then(local_address)
    }

    fn scheme(&self) -> Option<String> {
        self.properties.and_then(scheme)
    }

    fn version(&self) -> Option<String> {
        self.properties.and_then(version)
    }
}

/// A wrapper that implements the [`AttributesBinding`] trait for the different accessors for classy framework.
#[doc(hidden)]
pub struct AccessorAttributesBinding<'a> {
    accessor: &'a dyn HeadersAccessor,
    properties: Option<&'a dyn PropertyAccessor>,
}

impl<'a> AccessorAttributesBinding<'a> {
    /// Will create a binding that will resolve all properties of the attributes variable.
    pub fn new(handler: &'a dyn HeadersAccessor, properties: &'a dyn PropertyAccessor) -> Self {
        Self {
            accessor: handler,
            properties: Some(properties),
        }
    }

    /// Will create a binding that won't be able to resolve remote_address, local_address, scheme or version
    pub fn partial(accessor: &'a dyn HeadersAccessor) -> Self {
        Self {
            accessor,
            properties: None,
        }
    }
}

impl AttributesBinding for AccessorAttributesBinding<'_> {
    fn extract_headers(&self) -> HashMap<String, String> {
        self.accessor.headers().into_iter().collect()
    }

    fn extract_header(&self, key: &str) -> Option<String> {
        self.accessor.header(key)
    }

    fn remote_address(&self) -> Option<String> {
        self.properties.and_then(remote_address)
    }

    fn local_address(&self) -> Option<String> {
        self.properties.and_then(local_address)
    }

    fn scheme(&self) -> Option<String> {
        self.properties.and_then(scheme)
    }

    fn version(&self) -> Option<String> {
        self.properties.and_then(version)
    }
}

pub(crate) struct AttributesBindingAdapter<'a> {
    delegate: &'a dyn AttributesBinding,
}

impl<'a> AttributesBindingAdapter<'a> {
    pub fn new(delegate: &'a dyn AttributesBinding) -> Self {
        Self { delegate }
    }

    pub fn extract_headers(&self) -> HashMap<String, PelValue> {
        convert_maps(self.delegate.extract_headers())
    }

    pub fn extract_header(&self, key: &str) -> Option<PelValue> {
        convert_string(self.delegate.extract_header(key))
    }

    pub fn extract_query_params(&self) -> HashMap<String, PelValue> {
        convert_maps(self.delegate.extract_query_params())
    }

    pub fn method(&self) -> Option<PelValue> {
        convert_string(self.delegate.method())
    }

    pub fn path(&self) -> Option<PelValue> {
        convert_string(self.delegate.path())
    }

    pub fn uri(&self) -> Option<PelValue> {
        convert_string(self.delegate.uri())
    }

    pub fn remote_address(&self) -> Option<PelValue> {
        convert_string(self.delegate.remote_address())
    }

    pub fn local_address(&self) -> Option<PelValue> {
        convert_string(self.delegate.local_address())
    }

    pub fn query_string(&self) -> Option<PelValue> {
        convert_string(self.delegate.query_string())
    }

    pub fn scheme(&self) -> Option<PelValue> {
        convert_string(self.delegate.scheme())
    }

    pub fn version(&self) -> Option<PelValue> {
        convert_string(self.delegate.version())
    }

    pub fn status_code(&self) -> Option<PelValue> {
        self.delegate
            .status_code()
            .map(|s| PelValue::number(s as f64))
    }
}

fn fake_url(uri: String) -> Option<Url> {
    Url::parse("http://fake_base").ok()?.join(uri.as_str()).ok()
}
fn convert_maps(map: HashMap<String, String>) -> HashMap<String, PelValue> {
    map.into_iter()
        .map(|(key, val)| (key, PelValue::string(val)))
        .collect()
}

fn convert_string(val: Option<String>) -> Option<PelValue> {
    val.map(PelValue::string)
}

fn remote_address(properties: &dyn PropertyAccessor) -> Option<String> {
    properties
        .read_property(SOURCE_ADDRESS)
        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
}

fn local_address(properties: &dyn PropertyAccessor) -> Option<String> {
    properties
        .read_property(DESTINATION_ADDRESS)
        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
}

fn scheme(properties: &dyn PropertyAccessor) -> Option<String> {
    properties
        .read_property(REQUEST_SCHEME)
        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
}

fn version(properties: &dyn PropertyAccessor) -> Option<String> {
    properties
        .read_property(REQUEST_PROTOCOL)
        .map(|bytes| String::from_utf8_lossy(bytes.as_slice()).to_string())
}