//! An origin's side of http-delivery §5.3 and §6.2: the headers it sends for an
//! object, a head proof and a preflight, and its answer to a `Range` header (RFC 9110
//! §14). The `wai http origin` command serves files with these.

use super::{hex, MEDIA_TYPE};

/// The `Cache-Control` of an immutable resource (§5.3).
pub const IMMUTABLE: &str = "public, max-age=31536000, immutable, no-transform";

/// The headers an origin sends for a `GET` or `HEAD` of an object whose content hash
/// is `cid` (§5.3), with the object's `codecs` value when given. No
/// `Content-Encoding`, no `Vary`.
pub fn object_headers(cid: &[u8; 32], codecs: Option<&str>) -> Vec<(&'static str, String)> {
    let content_type = match codecs {
        Some(c) => format!("{MEDIA_TYPE}; codecs=\"{c}\""),
        None => MEDIA_TYPE.to_string(),
    };
    vec![
        ("Content-Type", content_type),
        ("Cache-Control", IMMUTABLE.to_string()),
        ("ETag", format!("\"{}\"", hex(cid))),
        ("Accept-Ranges", "bytes".to_string()),
        ("Access-Control-Allow-Origin", "*".to_string()),
        ("Access-Control-Expose-Headers", "Content-Range, Content-Length, ETag".to_string()),
        ("Timing-Allow-Origin", "*".to_string()),
    ]
}

/// The headers for an immutable sidecar (a head proof, a receipt file, a parameter
/// set) of the given content type (§6.2, §8.1, §8.5).
pub fn immutable_headers(content_type: &str) -> Vec<(&'static str, String)> {
    vec![
        ("Content-Type", content_type.to_string()),
        ("Cache-Control", IMMUTABLE.to_string()),
        ("Access-Control-Allow-Origin", "*".to_string()),
        ("Timing-Allow-Origin", "*".to_string()),
    ]
}

/// The headers for a playlist (§11): revalidated on every use, since a live playlist
/// changes from revision to revision; its receipt (`mr/`) is immutable.
pub fn playlist_headers(content_type: &str) -> Vec<(&'static str, String)> {
    vec![
        ("Content-Type", content_type.to_string()),
        ("Cache-Control", "no-cache, no-transform".to_string()),
        ("Access-Control-Allow-Origin", "*".to_string()),
        ("Timing-Allow-Origin", "*".to_string()),
    ]
}

/// The headers for an object listed only at a live edge, which an origin MAY serve
/// with a smaller `max-age` (§5.3): [`object_headers`] with that `Cache-Control`.
pub fn live_object_headers(cid: &[u8; 32], codecs: Option<&str>, max_age_s: u64) -> Vec<(&'static str, String)> {
    object_headers(cid, codecs)
        .into_iter()
        .map(|(k, v)| if k == "Cache-Control" { (k, format!("public, max-age={max_age_s}, no-transform")) } else { (k, v) })
        .collect()
}

/// The headers for a key log or module grants file (§8.4): JSON, `max-age=300`.
pub fn key_resource_headers() -> Vec<(&'static str, String)> {
    vec![
        ("Content-Type", "application/json".to_string()),
        ("Cache-Control", "max-age=300, no-transform".to_string()),
        ("Access-Control-Allow-Origin", "*".to_string()),
    ]
}

/// The headers for a receipt index `<cid>.wai.receipts` (§8.2): a mutable locator,
/// revalidated on every use, with an `ETag` of its own (its content hash).
pub fn receipt_index_headers(index: &[u8]) -> Vec<(&'static str, String)> {
    vec![
        ("Content-Type", "application/json".to_string()),
        ("Cache-Control", "no-cache, no-transform".to_string()),
        ("ETag", format!("\"{}\"", hex(&super::content_hash(index)))),
        ("Access-Control-Allow-Origin", "*".to_string()),
    ]
}

/// The headers for a key status (§8.4): `max-age` at most a quarter of the
/// `status_max_age_s` of the head it names, where the origin knows it; revalidated on
/// every use where it does not.
pub fn key_status_headers(max_age_s: Option<u64>) -> Vec<(&'static str, String)> {
    let cc = match max_age_s {
        Some(s) => format!("max-age={s}, no-transform"),
        None => "no-cache, no-transform".to_string(),
    };
    vec![
        ("Content-Type", "application/json".to_string()),
        ("Cache-Control", cc),
        ("Access-Control-Allow-Origin", "*".to_string()),
    ]
}

/// Whether a `Range` is honoured under `If-Range` (RFC 9110 §13.1.5): with no
/// `If-Range`, yes; with one, only when it is the representation's strong `etag`
/// exactly. A weak validator never matches, and a date is not used for an object.
pub fn if_range_allows(if_range: Option<&str>, etag: &str) -> bool {
    match if_range {
        None => true,
        Some(v) => v.trim() == etag,
    }
}

/// The headers that answer a CORS preflight for an object (§5.3).
pub fn preflight_headers() -> Vec<(&'static str, String)> {
    vec![
        ("Access-Control-Allow-Origin", "*".to_string()),
        ("Access-Control-Allow-Methods", "GET, HEAD".to_string()),
        ("Access-Control-Allow-Headers", "Range, Priority".to_string()),
        ("Access-Control-Max-Age", "86400".to_string()),
    ]
}

/// An answer to a request for a representation of `len` bytes.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Served {
    pub status: u16,
    /// `Content-Range`, on a `206` or a `416`.
    pub content_range: Option<String>,
    /// The bytes of the representation the body holds: empty on a `416`.
    pub body: std::ops::Range<u64>,
}

/// Answer `range` (a `Range` header value, or none) on a representation of `len`
/// bytes: `200` and the whole without one, with a multi-range request, or with one
/// this does not read (RFC 9110 §14.2 lets a server ignore it); `206` and the bytes
/// for one satisfiable range (a first and last position, an open end, or a suffix);
/// `416` with `bytes */len` for an unsatisfiable one.
pub fn serve(len: u64, range: Option<&str>) -> Served {
    let whole = Served { status: 200, content_range: None, body: 0..len };
    let unsatisfiable = Served { status: 416, content_range: Some(format!("bytes */{len}")), body: 0..0 };
    let Some(spec) = range.and_then(|r| r.trim().strip_prefix("bytes=")) else { return whole };
    if spec.contains(',') {
        return whole;
    }
    let Some((a, b)) = spec.trim().split_once('-') else { return whole };
    let (a, b) = (a.trim(), b.trim());
    let (first, last) = match (a.is_empty(), b.is_empty()) {
        (true, true) => return whole,
        (true, false) => {
            let Ok(suffix) = b.parse::<u64>() else { return whole };
            if suffix == 0 || len == 0 {
                return unsatisfiable;
            }
            (len.saturating_sub(suffix), len - 1)
        }
        (false, _) => {
            let Ok(first) = a.parse::<u64>() else { return whole };
            let last = if b.is_empty() {
                u64::MAX
            } else {
                match b.parse::<u64>() {
                    Ok(l) if l >= first => l,
                    _ => return whole,
                }
            };
            if first >= len {
                return unsatisfiable;
            }
            (first, last.min(len - 1))
        }
    };
    Served { status: 206, content_range: Some(format!("bytes {first}-{last}/{len}")), body: first..last + 1 }
}
