// CSRF Protection Middleware
//
// Modern CSRF protection based on:
// - Origin header validation (primary defense)
// - Fetch Metadata validation (Sec-Fetch-* headers)
// - Content-Type validation (for JSON APIs)
//
// Reference: https://blog.jxck.io/entries/2024-04-26/csrf.html
//
// Design principles:
// 1. Origin validation is the essential defense
// 2. SameSite Cookie is a secondary effect, not primary
// 3. Fetch Metadata provides additional context
// 4. CSRF tokens are optional second layer

// ============================================================================
// CSRF Configuration
// ============================================================================

///|
/// CSRF protection configuration
pub struct CsrfConfig {
  /// Allowed origins (required for cross-origin requests)
  allowed_origins : Array[String]
  /// Whether to check Fetch Metadata headers
  check_fetch_metadata : Bool
  /// Expected Content-Type for API requests (e.g., "application/json")
  expected_content_type : String?
  /// Whether to allow requests without Origin header (same-origin)
  allow_no_origin : Bool
  /// Custom error message
  error_message : String
}

///|
/// Default CSRF configuration (strict)
pub fn CsrfConfig::default() -> CsrfConfig {
  {
    allowed_origins: [],
    check_fetch_metadata: true,
    expected_content_type: None,
    allow_no_origin: true, // Same-origin requests don't send Origin
    error_message: "Forbidden",
  }
}

///|
/// Create config for single origin
pub fn CsrfConfig::for_origin(origin : String) -> CsrfConfig {
  { ..CsrfConfig::default(), allowed_origins: [origin] }
}

///|
/// Create config for multiple origins
pub fn CsrfConfig::for_origins(origins : Array[String]) -> CsrfConfig {
  { ..CsrfConfig::default(), allowed_origins: origins }
}

///|
/// Set allowed origins
pub fn CsrfConfig::with_origins(
  self : CsrfConfig,
  origins : Array[String],
) -> CsrfConfig {
  { ..self, allowed_origins: origins }
}

///|
/// Add single allowed origin
pub fn CsrfConfig::with_origin(
  self : CsrfConfig,
  origin : String,
) -> CsrfConfig {
  let origins = self.allowed_origins.copy()
  origins.push(origin)
  { ..self, allowed_origins: origins }
}

///|
/// Enable/disable Fetch Metadata check
pub fn CsrfConfig::with_fetch_metadata(
  self : CsrfConfig,
  enabled : Bool,
) -> CsrfConfig {
  { ..self, check_fetch_metadata: enabled }
}

///|
/// Set expected Content-Type
pub fn CsrfConfig::with_content_type(
  self : CsrfConfig,
  content_type : String,
) -> CsrfConfig {
  { ..self, expected_content_type: Some(content_type) }
}

///|
/// Set JSON Content-Type requirement
pub fn CsrfConfig::require_json(self : CsrfConfig) -> CsrfConfig {
  self.with_content_type("application/json")
}

///|
/// Set custom error message
pub fn CsrfConfig::with_error_message(
  self : CsrfConfig,
  message : String,
) -> CsrfConfig {
  { ..self, error_message: message }
}

// ============================================================================
// CSRF Result Types
// ============================================================================

///|
/// CSRF validation result
pub enum CsrfResult {
  /// Validation passed
  Valid
  /// Origin header missing but allowed (same-origin)
  ValidNoOrigin
  /// Origin header valid
  ValidOrigin(String)
  /// Validation failed with reason
  Invalid(CsrfError)
}

///|
/// CSRF validation error types
pub enum CsrfError {
  /// Origin header missing and not allowed
  MissingOrigin
  /// Origin not in allowed list
  InvalidOrigin(String)
  /// Sec-Fetch-Site indicates cross-site
  CrossSiteFetch
  /// Sec-Fetch-Mode indicates unsafe mode
  UnsafeFetchMode
  /// Content-Type mismatch
  InvalidContentType(String)
}

///|
pub fn CsrfError::to_string(self : CsrfError) -> String {
  match self {
    MissingOrigin => "Missing Origin header"
    InvalidOrigin(origin) => "Invalid Origin: \{origin}"
    CrossSiteFetch => "Cross-site fetch not allowed"
    UnsafeFetchMode => "Unsafe fetch mode"
    InvalidContentType(ct) => "Invalid Content-Type: \{ct}"
  }
}

// ============================================================================
// CSRF Middleware
// ============================================================================

///|
/// Create CSRF protection middleware with default config
/// Note: You must configure allowed origins for this to work properly
pub fn csrf() -> Middleware {
  csrf_with_config(CsrfConfig::default())
}

///|
/// Create CSRF protection middleware for single origin
pub fn csrf_for_origin(origin : String) -> Middleware {
  csrf_with_config(CsrfConfig::for_origin(origin))
}

///|
/// Create CSRF protection middleware with custom config
pub fn csrf_with_config(config : CsrfConfig) -> Middleware {
  Middleware(
    @mars.Handler(async fn(ctx) {
      // Skip safe methods (GET, HEAD, OPTIONS)
      if is_safe_method(ctx.meth().to_string()) {
        return // Continue
      }

      // Validate CSRF
      match validate_csrf(ctx, config) {
        Valid | ValidNoOrigin | ValidOrigin(_) => () // Continue
        Invalid(_) => ctx.text(config.error_message, status=403)
      }
    }),
  )
}

// ============================================================================
// Validation Logic
// ============================================================================

///|
/// Validate CSRF protection (delegates to pure functions)
fn validate_csrf(ctx : Ctx, config : CsrfConfig) -> CsrfResult {
  let origin = ctx.header("Origin")
  let referer = ctx.header("Referer")
  let sec_fetch_site = ctx.header("Sec-Fetch-Site")
  let content_type = ctx.header("Content-Type")
  validate_csrf_pure(origin~, referer~, sec_fetch_site~, content_type~, config~)
}

///|
/// Pure CSRF validation — no IO dependencies, fully testable
fn validate_csrf_pure(
  origin~ : String?,
  referer~ : String?,
  sec_fetch_site~ : String?,
  content_type~ : String?,
  config~ : CsrfConfig,
) -> CsrfResult {
  // Layer 1: Origin header validation (most important)
  let origin_result = validate_origin_pure(
    origin~,
    referer~,
    allowed_origins=config.allowed_origins,
    allow_no_origin=config.allow_no_origin,
  )
  match origin_result {
    Invalid(_) as err => return err
    _ => ()
  }

  // Layer 2: Fetch Metadata validation (if enabled)
  if config.check_fetch_metadata {
    let fetch_result = validate_fetch_metadata_pure(sec_fetch_site)
    match fetch_result {
      Invalid(_) as err => return err
      _ => ()
    }
  }

  // Layer 3: Content-Type validation (if configured)
  match config.expected_content_type {
    Some(expected) => {
      let ct_result = validate_content_type_pure(content_type, expected)
      match ct_result {
        Invalid(_) as err => return err
        _ => ()
      }
    }
    None => ()
  }
  origin_result
}

///|
/// Pure Origin validation — testable without Ctx
fn validate_origin_pure(
  origin~ : String?,
  referer~ : String?,
  allowed_origins~ : Array[String],
  allow_no_origin~ : Bool,
) -> CsrfResult {
  match origin {
    None =>
      if allow_no_origin {
        match referer {
          Some(referer) =>
            if is_referer_allowed(referer, allowed_origins) {
              ValidNoOrigin
            } else if allowed_origins.is_empty() {
              ValidNoOrigin
            } else {
              Invalid(InvalidOrigin(referer))
            }
          None => ValidNoOrigin
        }
      } else {
        Invalid(MissingOrigin)
      }
    Some(origin) =>
      if allowed_origins.is_empty() {
        Invalid(InvalidOrigin(origin))
      } else if allowed_origins.contains(origin) {
        ValidOrigin(origin)
      } else {
        Invalid(InvalidOrigin(origin))
      }
  }
}

///|
/// Check if Referer matches any allowed origin
/// SECURITY: Extract origin from Referer URL and compare exactly
/// (Previous implementation used has_prefix which was vulnerable to
///  attacks like http://allowed-origin.attacker.com)
pub fn is_referer_allowed(
  referer : String,
  allowed_origins : Array[String],
) -> Bool {
  // Extract origin from Referer URL
  // Referer format: scheme://host[:port]/path
  let referer_origin = extract_origin_from_url(referer)
  match referer_origin {
    Some(origin) => allowed_origins.contains(origin)
    None => false
  }
}

///|
/// Extract origin (scheme://host[:port]) from a URL
/// Returns None if URL is malformed
pub fn extract_origin_from_url(url : String) -> String? {
  // Find scheme separator
  let scheme_sep = match url.find("://") {
    Some(idx) => idx
    None => return None
  }

  // Find path separator after scheme
  let after_scheme = scheme_sep + 3
  guard after_scheme < url.length() else { return None }

  // Find end of host:port (first "/" after scheme://)
  let rest = url.unsafe_substring(start=after_scheme, end=url.length())
  let origin = match rest.find("/") {
    Some(path_sep) => url.unsafe_substring(start=0, end=after_scheme + path_sep)
    None => url // No path - entire URL is the origin
  }
  Some(origin)
}

///|
/// Pure Fetch Metadata validation — testable without Ctx
fn validate_fetch_metadata_pure(sec_fetch_site : String?) -> CsrfResult {
  match sec_fetch_site {
    Some("cross-site") => Invalid(CrossSiteFetch)
    Some("same-origin") | Some("same-site") | Some("none") => Valid
    None => Valid
    Some(_) => Valid
  }
}

///|
/// Pure Content-Type validation — testable without Ctx
fn validate_content_type_pure(
  content_type : String?,
  expected : String,
) -> CsrfResult {
  match content_type {
    Some(ct) =>
      if ct.has_prefix(expected) {
        Valid
      } else {
        Invalid(InvalidContentType(ct))
      }
    None => Invalid(InvalidContentType("(none)"))
  }
}

// ============================================================================
// Origin Validation Middleware (Standalone)
// ============================================================================

///|
/// Simple Origin-only validation middleware
/// Use this for simpler use cases where Fetch Metadata isn't needed
pub fn validate_origin_middleware(
  allowed_origins : Array[String],
) -> Middleware {
  Middleware(
    @mars.Handler(async fn(ctx) {
      // Skip safe methods
      if is_safe_method(ctx.meth().to_string()) {
        return // Continue
      }
      let origin = ctx.header("Origin")
      match origin {
        None => () // No Origin = same-origin, allow (continue)
        Some(o) =>
          if allowed_origins.contains(o) {
            () // Continue
          } else {
            ctx.text("Forbidden", status=403)
          }
      }
    }),
  )
}

// ============================================================================
// Fetch Metadata Middleware (Standalone)
// ============================================================================

///|
/// Fetch Metadata validation middleware
/// Blocks requests with Sec-Fetch-Site: cross-site
pub fn fetch_metadata() -> Middleware {
  Middleware(
    @mars.Handler(async fn(ctx) {
      // Skip safe methods
      if is_safe_method(ctx.meth().to_string()) {
        return // Continue
      }
      match validate_fetch_metadata_pure(ctx.header("Sec-Fetch-Site")) {
        Valid | ValidNoOrigin | ValidOrigin(_) => () // Continue
        Invalid(_) => ctx.text("Forbidden", status=403)
      }
    }),
  )
}

// ============================================================================
// Content-Type Middleware (Standalone)
// ============================================================================

///|
/// Require specific Content-Type for mutations
pub fn require_content_type(content_type : String) -> Middleware {
  Middleware(
    @mars.Handler(async fn(ctx) {
      // Skip safe methods
      if is_safe_method(ctx.meth().to_string()) {
        return // Continue
      }
      match
        validate_content_type_pure(ctx.header("Content-Type"), content_type) {
        Valid | ValidNoOrigin | ValidOrigin(_) => () // Continue
        Invalid(_) => ctx.text("Invalid Content-Type", status=400)
      }
    }),
  )
}

///|
/// Require JSON Content-Type for mutations
pub fn require_json_content_type() -> Middleware {
  require_content_type("application/json")
}