// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use arrayvec::ArrayVec;
use axum::body::Bytes;
use axum::extract::{MatchedPath, Path, Request, State};
use axum::http::header::CONTENT_TYPE;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Redirect};
use axum_extra::extract::CookieJar;
use base64::{Engine as B64Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use brotli::enc::BrotliEncoderParams;
use flate2::{
    Compression,
    write::{GzEncoder, ZlibEncoder},
};
use hyper::HeaderMap;
use ordinary_config::CompressionAlgorithm;
use ordinary_storage::{CacheCompression, CacheDependency, CacheRead, CacheWrite};
use ordinary_template::Template;
use ordinary_utils::middleware::{check_if_none_match, get_etag_hash};
use ordinary_utils::{GMT_FORMAT, get_host};
use std::io::Write;
use std::sync::Arc;
use time::{Duration, OffsetDateTime, UtcDateTime};

fn insert_headers(
    template: &Template,
    header_map: &mut HeaderMap,
    etag: &str,
    last_modified: &str,
) {
    if let Ok(etag) = HeaderValue::from_str(etag) {
        header_map.insert(header::ETAG, etag);
    }

    if let Ok(last_modified) = HeaderValue::from_str(last_modified) {
        header_map.insert(header::LAST_MODIFIED, last_modified);
    }

    if let Some(cache_control) = &template.cache_control
        && let Ok(cache_control) = HeaderValue::from_str(cache_control.as_str())
    {
        header_map.insert(header::CACHE_CONTROL, cache_control);
    }

    if let Some(cache) = &template.config.cache
        && let Some(http_cache) = &cache.http
        && let Some(expires_s) = http_cache.expires
    {
        let future = UtcDateTime::now() + Duration::seconds(expires_s.cast_signed());

        if let Ok(formatted) = future.format(&GMT_FORMAT)
            && let Ok(expires) = HeaderValue::from_str(formatted.as_str())
        {
            header_map.insert(header::EXPIRES, expires);
        }
    }
}

#[allow(clippy::too_many_lines)]
pub async fn get(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    jar: CookieJar,
    matched_path: MatchedPath,
    req: Request,
) -> Result<impl IntoResponse, (StatusCode, Redirect)> {
    let span = tracing::info_span!(
        "template",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
    );

    span.in_scope(|| {
        let uri = req.uri();
        let headers = req.headers();

        let Some(host) = get_host(headers, uri) else {
            tracing::error!("no host");
            return Ok(StatusCode::BAD_REQUEST.into_response());
        };

        let params = uri.query().map(ToString::to_string);

        let token: Vec<u8>;
        let mut claims = None;

        if let Some(idx) = state.template_route_map.get(matched_path.as_str()) {
            if let Some(template) = state.templates.get(*idx) {
                span.record("i", template.idx);
                span.record("nm", tracing::field::display(&template.config.name));

                if let Some(_check) = &template.config.protected {
                    let cookie_name = if state.secure_cookies {
                        "__Host-ORDINARY-ACCESS-TOKEN"
                    } else {
                        "ORDINARY-ACCESS-TOKEN"
                    };

                    token = if let Some(token) = jar.get(cookie_name) {
                        if let Ok(token) = b64.decode(token.value()) {
                            token
                        } else {
                            return Err((
                                StatusCode::UNAUTHORIZED,
                                Redirect::to("/accounts/access"),
                            ));
                        }
                    } else {
                        return Err((StatusCode::UNAUTHORIZED, Redirect::to("/accounts/access")));
                    };

                    match state.auth.verify_access_token(&token) {
                        Ok((_account, real_claims)) => {
                            claims = Some(real_claims);
                        }
                        Err(_) => {
                            return Err((
                                StatusCode::UNAUTHORIZED,
                                Redirect::to("/accounts/access"),
                            ));
                        }
                    }
                }

                let mut compression_opts = ArrayVec::<CacheCompression, 4>::new();

                let mut header_map = HeaderMap::with_capacity(11);

                let csp_lock = template.csp.read();
                if let Some(csp) = csp_lock.as_ref() {
                    header_map.insert(
                        template.reporting_endpoints.0.clone(),
                        template.reporting_endpoints.1.clone(),
                    );
                    header_map.insert(header::CONTENT_SECURITY_POLICY, csp.to_owned());
                }

                header_map.insert(
                    header::VARY,
                    HeaderValue::from_static(header::ACCEPT_ENCODING.as_str()),
                );

                let cache_span = tracing::info_span!("cache");

                // todo: normalize and join query for refs that are included in config.
                //
                // todo: including every permutation of the QueryString would lead to
                // todo: unnecessary cache duplicates.
                let path_and_query = req.uri().path();

                if let Some(cache) = &template.config.cache
                    && let Some(stored_cache) = &cache.stored
                {
                    cache_span.in_scope(|| {
                        if let Some(compression) = &stored_cache.compression
                            && let Some(compressions) = req.headers().get(header::ACCEPT_ENCODING)
                            && let Ok(compressions_str) = compressions.to_str()
                        {
                            for compression in compression {
                                match compression {
                                    CompressionAlgorithm::Brotli => {
                                        if compressions_str.contains("br") {
                                            compression_opts.push(CacheCompression::Brotli);
                                        }
                                    }
                                    CompressionAlgorithm::Deflate => {
                                        if compressions_str.contains("deflate") {
                                            compression_opts.push(CacheCompression::Deflate);
                                        }
                                    }
                                    CompressionAlgorithm::Zstd { level } => {
                                        if compressions_str.contains("zstd") {
                                            compression_opts
                                                .push(CacheCompression::Zstd { level: *level });
                                        }
                                    }
                                    CompressionAlgorithm::Gzip => {
                                        if compressions_str.contains("gzip") {
                                            compression_opts.push(CacheCompression::Gzip);
                                        }
                                    }
                                }
                            }
                        }
                    });

                    if let Ok(Some((hit, compression))) = cache_span.in_scope(|| {
                        state.storage.cache.check(
                            &CacheRead::Template,
                            &compression_opts,
                            template.idx,
                            path_and_query,
                        )
                    }) && let Ok(root) = flexbuffers::Reader::get_root(hit.as_ref())
                    {
                        let root_vec = root.as_vector();

                        let etag = root_vec.idx(1).as_str();
                        let last_modified = root_vec.idx(2).as_str();

                        insert_headers(template, &mut header_map, etag, last_modified);

                        return if let Some(etag) = check_if_none_match(req.headers(), etag)
                            && let Ok(etag_header) = HeaderValue::from_str(etag)
                        {
                            header_map.insert(header::ETAG, etag_header);

                            Ok((StatusCode::NOT_MODIFIED, header_map).into_response())
                        } else if let Some(if_modified_since) =
                            req.headers().get(header::IF_MODIFIED_SINCE)
                            && let Ok(if_modified_since_str) = if_modified_since.to_str()
                            && let Ok(if_modified_since) =
                                OffsetDateTime::parse(if_modified_since_str, &GMT_FORMAT)
                            && let Ok(last_modified) =
                                OffsetDateTime::parse(if_modified_since_str, &GMT_FORMAT)
                            && if_modified_since >= last_modified
                        {
                            Ok((StatusCode::NOT_MODIFIED, header_map).into_response())
                        } else {
                            if let Some(compression) = compression {
                                header_map.insert(
                                    header::CONTENT_ENCODING,
                                    HeaderValue::from_static(compression.as_str()),
                                );
                            }

                            header_map.insert(CONTENT_TYPE, template.mime.clone());

                            let res = root_vec.idx(3).as_blob();

                            Ok((StatusCode::OK, header_map, Bytes::copy_from_slice(res.0))
                                .into_response())
                        };
                    }
                }

                match template.render(
                    host.as_str(),
                    req.uri().path().into(),
                    params.clone(),
                    None,
                    None,
                    &claims,
                ) {
                    Ok(mut res) => {
                        let mut etag = cache_span.in_scope(|| {
                            if let Some(config) = &template.config.cache
                                && let Some(http_cache) = &config.http
                            {
                                get_etag_hash(res.as_ref(), Some(http_cache))
                            } else {
                                get_etag_hash(res.as_ref(), None)
                            }
                        });

                        let Ok(last_modified) = UtcDateTime::now().format(&GMT_FORMAT) else {
                            return Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response());
                        };

                        let last_modified_str = last_modified.as_str();

                        let mut cache_compression = None;

                        if let Some(cache) = &template.config.cache
                            && let Some(stored_cache) = &cache.stored
                        {
                            cache_span.in_scope(|| {
                                if let Some(compression) = compression_opts.first() {
                                    match compression {
                                        CacheCompression::Gzip => {
                                            let mut encoder =
                                                GzEncoder::new(Vec::new(), Compression::default());

                                            match encoder.write_all(res.as_ref()) {
                                                Ok(()) => match encoder.finish() {
                                                    Ok(compressed) => {
                                                        res = Bytes::copy_from_slice(
                                                            compressed.as_slice(),
                                                        );
                                                    }
                                                    Err(err) => tracing::error!(%err),
                                                },
                                                Err(err) => tracing::error!(%err),
                                            }
                                        }
                                        CacheCompression::Zstd { level } => {
                                            match zstd::stream::encode_all(
                                                std::io::Cursor::new(res.as_ref()),
                                                i32::from(*level),
                                            ) {
                                                Ok(compressed) => {
                                                    res = Bytes::copy_from_slice(
                                                        compressed.as_slice(),
                                                    );
                                                }
                                                Err(err) => tracing::error!(%err),
                                            }
                                        }
                                        CacheCompression::Brotli => {
                                            let mut compressed = Vec::new();

                                            let params = BrotliEncoderParams::default();
                                            match brotli::enc::BrotliCompress(
                                                &mut res.as_ref(),
                                                &mut compressed,
                                                &params,
                                            ) {
                                                Ok(_) => {
                                                    res = Bytes::copy_from_slice(
                                                        compressed.as_slice(),
                                                    );
                                                }
                                                Err(err) => tracing::error!(%err),
                                            }
                                        }
                                        CacheCompression::Deflate => {
                                            let mut encoder = ZlibEncoder::new(
                                                Vec::new(),
                                                Compression::default(),
                                            );

                                            match encoder.write_all(res.as_ref()) {
                                                Ok(()) => match encoder.finish() {
                                                    Ok(compressed) => {
                                                        res = Bytes::copy_from_slice(
                                                            compressed.as_slice(),
                                                        );
                                                    }
                                                    Err(err) => tracing::error!(%err),
                                                },
                                                Err(err) => tracing::error!(%err),
                                            }
                                        }
                                    }

                                    cache_compression = Some(compression);
                                    etag.push(compression.as_char());
                                }

                                if let Err(err) = state.storage.cache.write(
                                    CacheWrite::Template(etag.as_str(), last_modified_str, &res),
                                    cache_compression,
                                    template.idx,
                                    stored_cache,
                                    path_and_query,
                                    // todo: pass in `Model` dependency UUIDs
                                    if template.config.content.is_some() {
                                        vec![CacheDependency::Content]
                                    } else {
                                        vec![]
                                    },
                                ) {
                                    tracing::error!(%err, "failed to write to cache");
                                }
                            });
                        }

                        let etag_str = etag.as_str();

                        insert_headers(template, &mut header_map, etag_str, last_modified_str);

                        return if let Some(etag) = check_if_none_match(req.headers(), etag_str)
                            && let Ok(etag_header) = HeaderValue::from_str(etag)
                        {
                            header_map.insert(header::ETAG, etag_header);

                            Ok((StatusCode::NOT_MODIFIED, header_map).into_response())
                        } else {
                            if let Some(compression) = cache_compression {
                                header_map.insert(
                                    header::CONTENT_ENCODING,
                                    HeaderValue::from_static(compression.as_str()),
                                );
                            }

                            header_map.insert(CONTENT_TYPE, template.mime.clone());

                            Ok((StatusCode::OK, header_map, res).into_response())
                        };
                    }
                    Err(err) => tracing::error!("{err}"),
                }
            }
        } else {
            tracing::warn!("no template for route '{:?}'", matched_path);
        }

        if let Some(idx) = state.error_template_idx
            && let Some(err_template) = state.templates.get(idx as usize)
        {
            match err_template.render(
                host.as_str(),
                "/error".into(),
                params,
                Some(("no template for this route".into(), 404)),
                None,
                &claims,
            ) {
                Ok(res) => {
                    return Ok((
                        StatusCode::NOT_FOUND,
                        [(CONTENT_TYPE, err_template.mime.clone())],
                        res,
                    )
                        .into_response());
                }
                Err(err) => tracing::error!("{err}"),
            }
        }

        Ok((
            StatusCode::NOT_FOUND,
            [(CONTENT_TYPE, "text/plain")],
            Bytes::copy_from_slice(b"no template for the specified route"),
        )
            .into_response())
    })
}

pub async fn query(
    State(state): State<Arc<crate::server::OrdinaryAppServerState>>,
    Path((idx, path)): Path<(u8, String)>,
    req: Request,
) -> impl IntoResponse {
    let span = tracing::info_span!(
        "template",
        i = tracing::field::Empty,
        nm = tracing::field::Empty,
    );

    span.in_scope(|| {
        let uri = req.uri();
        let headers = req.headers();

        let Some(host) = get_host(headers, uri) else {
            tracing::error!("no host");
            return StatusCode::BAD_REQUEST.into_response();
        };

        let token: Vec<u8>;
        let mut claims = None;

        if let Some(template) = state.templates.get(idx as usize) {
            span.record("i", template.idx);
            span.record("nm", tracing::field::display(&template.config.name));

            if let Some(_check) = &template.config.protected
                && let Some(val) = req.headers().get("authorization")
                && let Ok(str_val) = val.to_str()
                && let Some(b64_token) = str_val.strip_prefix("Bearer ")
                && let Ok(t) = b64.decode(b64_token)
            {
                token = t;
                match state.auth.verify_access_token(&token) {
                    Ok((_account, real_claims)) => {
                        // todo: validate claims against the `_check`
                        claims = Some(real_claims);
                    }
                    Err(err) => {
                        tracing::error!("{err}");
                        return (
                            StatusCode::UNAUTHORIZED,
                            Bytes::copy_from_slice(b"unauthorized"),
                        )
                            .into_response();
                    }
                }
            }

            let params = req.uri().query().map(ToString::to_string);

            match template.query(host.as_str(), path, params, None, None, &claims) {
                Ok(res) => {
                    let mut header_map = HeaderMap::with_capacity(11);

                    let csp_lock = template.csp.read();
                    if let Some(csp) = csp_lock.as_ref() {
                        header_map.insert(
                            template.reporting_endpoints.0.clone(),
                            template.reporting_endpoints.1.clone(),
                        );
                        header_map.insert(header::CONTENT_SECURITY_POLICY, csp.to_owned());
                    }

                    header_map.insert(
                        header::VARY,
                        HeaderValue::from_static(header::ACCEPT_ENCODING.as_str()),
                    );

                    let etag = if let Some(config) = &template.config.cache
                        && let Some(http_cache) = &config.http
                    {
                        get_etag_hash(res.as_ref(), Some(http_cache))
                    } else {
                        get_etag_hash(res.as_ref(), None)
                    };

                    let etag_str = etag.as_str();

                    let Ok(last_modified) = UtcDateTime::now().format(&GMT_FORMAT) else {
                        return StatusCode::INTERNAL_SERVER_ERROR.into_response();
                    };

                    insert_headers(template, &mut header_map, etag_str, last_modified.as_str());

                    return if let Some(etag) = check_if_none_match(req.headers(), etag_str)
                        && let Ok(etag_header) = HeaderValue::from_str(etag)
                    {
                        header_map.insert(header::ETAG, etag_header);

                        (StatusCode::NOT_MODIFIED, header_map).into_response()
                    } else {
                        (StatusCode::OK, header_map, res).into_response()
                    };
                }
                Err(err) => {
                    tracing::error!("{err}");
                }
            }
        } else {
            tracing::warn!("no template for index '{idx}'");
        }
        (StatusCode::NOT_FOUND, Bytes::copy_from_slice(b"not found")).into_response()
    })
}
