use crate::AppState;
use crate::webserver::request_variables::SetVariablesMap;
use crate::webserver::server_timing::ServerTiming;
use actix_multipart::Multipart;
use actix_multipart::form::FieldReader;
use actix_multipart::form::Limits;
use actix_multipart::form::bytes::Bytes;
use actix_multipart::form::tempfile::TempFile;
use actix_web::FromRequest;
use actix_web::HttpMessage as _;
use actix_web::HttpRequest;
use actix_web::dev::ServiceRequest;
use actix_web::http::header::CONTENT_TYPE;
use actix_web::http::header::Header;
use actix_web::web;
use actix_web::web::Form;
use actix_web_httpauth::headers::authorization::Authorization;
use actix_web_httpauth::headers::authorization::Basic;
use anyhow::Context;
use anyhow::anyhow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::net::IpAddr;
use std::rc::Rc;
use std::sync::Arc;
use tokio_stream::StreamExt;

use super::oidc::OidcClaims;
use super::request_variables::ParamMap;
use super::request_variables::param_map;
use super::{ActixErrorStatusExt, StatusCodeResultExt};

#[derive(Debug)]
pub struct RequestInfo {
    pub method: actix_web::http::Method,
    pub path: String,
    pub protocol: String,
    pub url_params: ParamMap,
    pub post_variables: ParamMap,
    pub uploaded_files: Rc<HashMap<String, TempFile>>,
    pub headers: ParamMap,
    pub client_ip: Option<IpAddr>,
    pub cookies: ParamMap,
    pub basic_auth: Option<Basic>,
    pub app_state: Arc<AppState>,
    pub raw_body: Option<Vec<u8>>,
    pub oidc_claims: Option<OidcClaims>,
    pub server_timing: Arc<super::server_timing::ServerTiming>,
}

#[derive(Debug)]
pub struct ExecutionContext {
    pub request: Rc<RequestInfo>,
    pub set_variables: RefCell<SetVariablesMap>,
    pub clone_depth: u8,
}

impl ExecutionContext {
    #[must_use]
    pub fn new(request: RequestInfo) -> Self {
        Self {
            request: Rc::new(request),
            set_variables: RefCell::new(SetVariablesMap::new()),
            clone_depth: 0,
        }
    }

    #[must_use]
    pub fn fork(&self) -> Self {
        Self {
            request: Rc::clone(&self.request),
            set_variables: RefCell::new(self.set_variables.borrow().clone()),
            clone_depth: self.clone_depth + 1,
        }
    }

    #[must_use]
    pub fn fork_with_variables(&self, variables: SetVariablesMap) -> Self {
        Self {
            request: Rc::clone(&self.request),
            set_variables: RefCell::new(variables),
            clone_depth: self.clone_depth + 1,
        }
    }

    pub fn request(&self) -> &RequestInfo {
        self.request.as_ref()
    }
}

impl Clone for ExecutionContext {
    fn clone(&self) -> Self {
        self.fork()
    }
}

impl std::ops::Deref for ExecutionContext {
    type Target = RequestInfo;

    fn deref(&self) -> &Self::Target {
        self.request()
    }
}

impl<'a> From<&'a ExecutionContext> for &'a RequestInfo {
    fn from(ctx: &'a ExecutionContext) -> Self {
        ctx.request()
    }
}

pub(crate) async fn extract_request_info(
    req: &mut ServiceRequest,
    app_state: Arc<AppState>,
    server_timing: ServerTiming,
) -> anyhow::Result<ExecutionContext> {
    let (http_req, payload) = req.parts_mut();
    let method = http_req.method().clone();
    let protocol = http_req.connection_info().scheme().to_string();
    let config = &app_state.config;
    let (post_variables, uploaded_files, raw_body) =
        extract_post_data(http_req, payload, config).await?;
    let headers = req.headers().iter().map(|(name, value)| {
        (
            name.to_string(),
            String::from_utf8_lossy(value.as_bytes()).to_string(),
        )
    });
    let get_variables = web::Query::<Vec<(String, String)>>::from_query(req.query_string())
        .map(web::Query::into_inner)
        .unwrap_or_default();
    let client_ip = req.peer_addr().map(|addr| addr.ip());

    let raw_cookies = req.cookies();
    let cookies = raw_cookies
        .iter()
        .flat_map(|c| c.iter())
        .map(|cookie| (cookie.name().to_string(), cookie.value().to_string()));

    let basic_auth = Authorization::<Basic>::parse(req)
        .ok()
        .map(Authorization::into_scheme);

    let oidc_claims: Option<OidcClaims> = req.extensions().get::<OidcClaims>().cloned();

    Ok(ExecutionContext::new(RequestInfo {
        method,
        path: req.path().to_string(),
        headers: param_map(headers),
        url_params: param_map(get_variables),
        post_variables: param_map(post_variables),
        uploaded_files: Rc::new(HashMap::from_iter(uploaded_files)),
        client_ip,
        cookies: param_map(cookies),
        basic_auth,
        app_state,
        protocol,
        raw_body,
        oidc_claims,
        server_timing: Arc::new(server_timing),
    }))
}

async fn extract_post_data(
    http_req: &mut actix_web::HttpRequest,
    payload: &mut actix_web::dev::Payload,
    config: &crate::app_config::AppConfig,
) -> anyhow::Result<(
    Vec<(String, String)>,
    Vec<(String, TempFile)>,
    Option<Vec<u8>>,
)> {
    let content_type = http_req
        .headers()
        .get(&CONTENT_TYPE)
        .map(AsRef::as_ref)
        .unwrap_or_default();
    if content_type.starts_with(b"application/x-www-form-urlencoded") {
        let vars = extract_urlencoded_post_variables(http_req, payload).await?;
        Ok((vars, Vec::new(), None))
    } else if content_type.starts_with(b"multipart/form-data") {
        let (vars, files) = extract_multipart_post_data(http_req, payload, config).await?;
        Ok((vars, files, None))
    } else {
        let body = actix_web::web::Bytes::from_request(http_req, payload)
            .await
            .map(|bytes| bytes.to_vec())
            .unwrap_or_default();
        Ok((Vec::new(), Vec::new(), Some(body)))
    }
}

async fn extract_urlencoded_post_variables(
    http_req: &mut actix_web::HttpRequest,
    payload: &mut actix_web::dev::Payload,
) -> anyhow::Result<Vec<(String, String)>> {
    Form::<Vec<(String, String)>>::from_request(http_req, payload)
        .await
        .map(Form::into_inner)
        .with_actix_error_status()
        .context("could not parse request as urlencoded form data")
}

async fn extract_multipart_post_data(
    http_req: &mut actix_web::HttpRequest,
    payload: &mut actix_web::dev::Payload,
    config: &crate::app_config::AppConfig,
) -> anyhow::Result<(Vec<(String, String)>, Vec<(String, TempFile)>)> {
    let mut post_variables = Vec::new();
    let mut uploaded_files = Vec::new();

    let mut multipart = Multipart::from_request(http_req, payload)
        .await
        .with_actix_error_status()
        .context("could not parse request as multipart form data")?;

    let mut limits = Limits::new(config.max_uploaded_file_size, config.max_uploaded_file_size);
    log::trace!(
        "Parsing multipart form data with a {:?} KiB limit",
        limits.total_limit_remaining / 1024
    );

    while let Some(part) = multipart.next().await {
        let field = part
            .with_response_status()
            .context("unable to read form field")?;
        let content_disposition = field
            .content_disposition()
            .ok_or_else(|| anyhow!("missing Content-Disposition in form field"))
            .with_status(actix_web::http::StatusCode::BAD_REQUEST)?;
        // test if field is a file
        let filename = content_disposition.get_filename();
        let field_name = content_disposition
            .get_name()
            .unwrap_or_default()
            .to_string();
        log::trace!("Parsing multipart field: {field_name}");
        if let Some(filename) = filename {
            log::debug!("Extracting file: {field_name} ({filename})");
            let extracted = extract_file(http_req, field, &mut limits)
                .await
                .with_context(|| {
                    format!(
                        "Failed to extract file {field_name:?}. Max file size: {} kiB",
                        config.max_uploaded_file_size / 1_024
                    )
                })?;
            log::trace!(
                "Extracted file {field_name} to \"{}\"",
                extracted.file.path().display()
            );
            if is_file_field_empty(&extracted).await? {
                log::debug!("Ignoring empty file field: {field_name}");
                continue;
            }
            uploaded_files.push((field_name, extracted));
        } else {
            let text_contents = extract_text(http_req, field, &mut limits).await?;
            log::trace!("Extracted field as text: {field_name} = {text_contents:?}");
            post_variables.push((field_name, text_contents));
        }
    }
    Ok((post_variables, uploaded_files))
}

async fn extract_text(
    req: &HttpRequest,
    field: actix_multipart::Field,
    limits: &mut Limits,
) -> anyhow::Result<String> {
    // field is an async stream of Result<Bytes> objects, we collect them into a Vec<u8>
    let data = Bytes::read_field(req, field, limits)
        .await
        .map(|bytes| bytes.data)
        .with_response_status()
        .context("failed to read form field data")?;
    String::from_utf8(data.to_vec())
        .with_status(actix_web::http::StatusCode::BAD_REQUEST)
        .context("could not parse multipart form field as utf-8 text")
}

async fn extract_file(
    req: &HttpRequest,
    field: actix_multipart::Field,
    limits: &mut Limits,
) -> anyhow::Result<TempFile> {
    // extract a tempfile from the field
    let file = TempFile::read_field(req, field, limits)
        .await
        .with_response_status()
        .context("failed to save uploaded file")?;
    Ok(file)
}

/// file upload form fields that are left blank result in the browser sending an empty file, with a mime type of application/octet-stream.
/// We don't want to treat this the same as actual empty files, so we check for this case.
async fn is_file_field_empty(
    uploaded_file: &actix_multipart::form::tempfile::TempFile,
) -> anyhow::Result<bool> {
    Ok(
        uploaded_file.content_type == Some(mime_guess::mime::APPLICATION_OCTET_STREAM)
            && uploaded_file.file_name.as_deref().is_none_or(str::is_empty)
            && tokio::fs::metadata(&uploaded_file.file.path()).await?.len() == 0,
    )
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::webserver::single_or_vec::SingleOrVec;
    use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming};
    use actix_web::{http::header::ContentType, test::TestRequest};

    #[actix_web::test]
    async fn test_extract_empty_request() {
        let config =
            serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
        let mut service_request = TestRequest::default().to_srv_request();
        let app_data = Arc::new(AppState::init(&config).await.unwrap());
        let server_timing = ServerTiming::default();
        let request_ctx = extract_request_info(&mut service_request, app_data, server_timing)
            .await
            .unwrap();
        let request_info = request_ctx.request();
        assert_eq!(request_info.post_variables.len(), 0);
        assert_eq!(request_info.uploaded_files.len(), 0);
        assert_eq!(request_info.url_params.len(), 0);
    }

    #[actix_web::test]
    async fn test_extract_urlencoded_request() {
        let config =
            serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
        let mut service_request = TestRequest::get()
            .uri("/?my_array[]=5")
            .insert_header(ContentType::form_url_encoded())
            .set_payload("my_array[]=3&my_array[]=Hello%20World&repeated=1&repeated=2")
            .to_srv_request();
        let app_data = Arc::new(AppState::init(&config).await.unwrap());
        let server_timing = ServerTiming::default();
        let request_ctx = extract_request_info(&mut service_request, app_data, server_timing)
            .await
            .unwrap();
        let request_info = request_ctx.request();
        assert_eq!(
            request_info.post_variables,
            vec![
                (
                    "my_array".to_string(),
                    SingleOrVec::Vec(vec!["3".to_string(), "Hello World".to_string()])
                ),
                ("repeated".to_string(), SingleOrVec::Single("2".to_string())), // without brackets, only the last value is kept
            ]
            .into_iter()
            .collect::<ParamMap>()
        );
        assert_eq!(request_info.uploaded_files.len(), 0);
        assert_eq!(
            request_info.url_params,
            vec![(
                "my_array".to_string(),
                SingleOrVec::Vec(vec!["5".to_string()])
            )] // with brackets, even if there is only one value, it is kept as a vector
            .into_iter()
            .collect::<ParamMap>()
        );
    }

    #[actix_web::test]
    async fn test_extract_multipart_form_data() {
        crate::telemetry::init_test_logging();
        let config =
            serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
        let mut service_request = TestRequest::get()
            .insert_header(("content-type", "multipart/form-data;boundary=xxx"))
            .set_payload(
                "--xxx\r\n\
                Content-Disposition: form-data; name=\"my_array[]\"\r\n\
                Content-Type: text/plain\r\n\
                \r\n\
                3\r\n\
                --xxx\r\n\
                Content-Disposition: form-data; name=\"my_uploaded_file\"; filename=\"test.txt\"\r\n\
                Content-Type: text/plain\r\n\
                \r\n\
                Hello World\r\n\
                --xxx--\r\n"
            )
            .to_srv_request();
        let app_data = Arc::new(AppState::init(&config).await.unwrap());
        let server_timing = ServerTiming::enabled(false);
        let request_ctx = extract_request_info(&mut service_request, app_data, server_timing)
            .await
            .unwrap();
        let request_info = request_ctx.request();
        assert_eq!(
            request_info.post_variables,
            vec![(
                "my_array".to_string(),
                SingleOrVec::Vec(vec!["3".to_string()])
            ),]
            .into_iter()
            .collect::<ParamMap>()
        );
        assert_eq!(request_info.uploaded_files.len(), 1);
        let my_upload = &request_info.uploaded_files["my_uploaded_file"];
        assert_eq!(my_upload.file_name.as_ref().unwrap(), "test.txt");
        assert_eq!(request_info.url_params.len(), 0);
        assert_eq!(std::fs::read(&my_upload.file).unwrap(), b"Hello World");
        assert_eq!(request_info.url_params.len(), 0);
    }
}
