///|
/// Validated application configuration profiles.
/// Keeping policy validation in the library prevents silently accepting
/// impossible windows or unsafe thresholds from CLI and service callers.
pub(all) enum ConfigurationEnvironment {
  ConfigurationDevelopment
  ConfigurationProduction
  ConfigurationBenchmark
  ConfigurationResearch
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct ApplicationConfig {
  mut environment : ConfigurationEnvironment
  hrv : HrvConfig
  analysis : AnalysisOptions
  ingest : WearableIngestConfig
  gate : QualityGatePolicy
  load : LoadModelConfig
  forecast : ForecastConfig
  subject_retention_days : Int
  mut deterministic : Bool
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct ConfigIssue {
  key : String
  severity : String
  message : String
  observed : Double
  expected : Double
} derive(FromJson, ToJson, Debug, Eq)

///|
pub(all) struct ConfigValidation {
  valid : Bool
  issues : Array[ConfigIssue]
  normalized : ApplicationConfig
} derive(FromJson, ToJson, Debug, Eq)

///|
pub fn ApplicationConfig::default() -> ApplicationConfig {
  {
    environment: ConfigurationProduction,
    hrv: HrvConfig::default(),
    analysis: AnalysisOptions::default(),
    ingest: WearableIngestConfig::default(),
    gate: QualityGatePolicy::default(),
    load: LoadModelConfig::default(),
    forecast: ForecastConfig::default(),
    subject_retention_days: 365,
    deterministic: true,
  }
}

///|
pub fn application_config_for(
  environment : ConfigurationEnvironment,
) -> ApplicationConfig {
  let base = ApplicationConfig::default()
  match environment {
    ConfigurationDevelopment => {
      base.environment = ConfigurationDevelopment
      base
    }
    ConfigurationBenchmark => {
      base.environment = ConfigurationBenchmark
      base
    }
    ConfigurationResearch => {
      base.environment = ConfigurationResearch
      base
    }
    ConfigurationProduction => base
  }
}

///|
fn config_issue(
  key : String,
  severity : String,
  message : String,
  observed : Double,
  expected : Double,
) -> ConfigIssue {
  { key, severity, message, observed, expected }
}

///|
pub fn validate_application_config(
  config : ApplicationConfig,
) -> ConfigValidation {
  let issues = []
  if config.ingest.minimum_quality < 0.0 || config.ingest.minimum_quality > 1.0 {
    issues.push(
      config_issue(
        "ingest.minimum_quality",
        "error",
        "quality floor must be in [0,1]",
        config.ingest.minimum_quality,
        0.70,
      ),
    )
  }
  if config.gate.minimum_samples < 1 {
    issues.push(
      config_issue(
        "gate.minimum_samples",
        "error",
        "minimum sample count must be positive",
        config.gate.minimum_samples.to_double(),
        32.0,
      ),
    )
  }
  if config.gate.minimum_clean_ratio < 0.0 ||
    config.gate.minimum_clean_ratio > 1.0 {
    issues.push(
      config_issue(
        "gate.minimum_clean_ratio",
        "error",
        "clean ratio must be in [0,1]",
        config.gate.minimum_clean_ratio,
        0.70,
      ),
    )
  }
  if config.analysis.sample_rate_hz <= 0.0 {
    issues.push(
      config_issue(
        "analysis.sample_rate_hz",
        "error",
        "sample rate must be positive",
        config.analysis.sample_rate_hz,
        4.0,
      ),
    )
  }
  if config.analysis.segment_size < 2 || config.analysis.segment_hop < 1 {
    issues.push(
      config_issue(
        "analysis.windows",
        "error",
        "segment size and hop must be positive",
        config.analysis.segment_size.to_double(),
        60.0,
      ),
    )
  }
  if config.load.acute_window_days < 1 ||
    config.load.chronic_window_days < config.load.acute_window_days {
    issues.push(
      config_issue(
        "load.windows",
        "error",
        "chronic window must be at least the acute window",
        config.load.chronic_window_days.to_double(),
        28.0,
      ),
    )
  }
  if config.forecast.horizon < 0 || config.forecast.minimum_history < 1 {
    issues.push(
      config_issue(
        "forecast.history",
        "error",
        "forecast horizon and history must be non-negative and positive",
        config.forecast.horizon.to_double(),
        7.0,
      ),
    )
  }
  if config.subject_retention_days < 0 {
    issues.push(
      config_issue(
        "subject_retention_days",
        "error",
        "retention cannot be negative",
        config.subject_retention_days.to_double(),
        365.0,
      ),
    )
  }
  {
    valid: issues.filter(issue => issue.severity == "error").length() == 0,
    issues,
    normalized: config,
  }
}

///|
pub fn configuration_environment_name(
  environment : ConfigurationEnvironment,
) -> String {
  match environment {
    ConfigurationDevelopment => "development"
    ConfigurationProduction => "production"
    ConfigurationBenchmark => "benchmark"
    ConfigurationResearch => "research"
  }
}

///|
pub fn configuration_is_strict(environment : ConfigurationEnvironment) -> Bool {
  environment == ConfigurationProduction ||
  environment == ConfigurationBenchmark
}

///|
pub fn config_validation_csv(validation : ConfigValidation) -> String {
  let grid = [["key", "severity", "message", "observed", "expected"]]
  for issue in validation.issues {
    grid.push([
      issue.key,
      issue.severity,
      issue.message,
      issue.observed.to_string(),
      issue.expected.to_string(),
    ])
  }
  to_csv(grid)
}

///|
pub fn config_validation_message(validation : ConfigValidation) -> String {
  if validation.valid {
    "application configuration is valid"
  } else {
    "\{validation.issues.length().to_string()} configuration issues require attention"
  }
}

///|
pub fn config_feature_vector(config : ApplicationConfig) -> Array[Double] {
  [
    config.ingest.minimum_quality,
    config.ingest.maximum_gap_seconds,
    config.gate.minimum_samples.to_double(),
    config.gate.minimum_clean_ratio,
    config.analysis.sample_rate_hz,
    config.analysis.segment_size.to_double(),
    config.analysis.segment_hop.to_double(),
    config.load.acute_window_days.to_double(),
    config.load.chronic_window_days.to_double(),
    config.forecast.horizon.to_double(),
    config.forecast.minimum_history.to_double(),
    config.subject_retention_days.to_double(),
    if config.deterministic {
      1.0
    } else {
      0.0
    },
  ]
}

///|
pub fn config_is_deterministic(config : ApplicationConfig) -> Bool {
  config.deterministic
}

///|
pub fn config_quality_floor(config : ApplicationConfig) -> Double {
  config.ingest.minimum_quality.min(config.gate.minimum_signal_quality)
}

///|
pub fn config_window_days(config : ApplicationConfig) -> Array[Int] {
  [config.load.acute_window_days, config.load.chronic_window_days]
}

///|
pub fn config_requires_frequency(config : ApplicationConfig) -> Bool {
  config.gate.require_frequency
}

///|
pub fn config_requires_nonlinear(config : ApplicationConfig) -> Bool {
  config.gate.require_nonlinear
}

///|
pub fn config_maximum_gap(config : ApplicationConfig) -> Double {
  config.ingest.maximum_gap_seconds
}

///|
pub fn config_forecast_horizon(config : ApplicationConfig) -> Int {
  config.forecast.horizon
}

///|
pub fn config_retention_seconds(config : ApplicationConfig) -> Double {
  config.subject_retention_days.to_double() * 86400.0
}

///|
pub fn config_summary(config : ApplicationConfig) -> String {
  "\{configuration_environment_name(config.environment)}: quality=\{config_quality_floor(config).to_string()} windows=\{config.load.acute_window_days.to_string()}/\{config.load.chronic_window_days.to_string()} forecast=\{config.forecast.horizon.to_string()}"
}

///|
pub fn config_validation_has_errors(validation : ConfigValidation) -> Bool {
  validation.issues.any(issue => issue.severity == "error")
}

///|
pub fn config_validation_warning_count(validation : ConfigValidation) -> Int {
  validation.issues.filter(issue => issue.severity == "warning").length()
}

///|
pub fn config_validation_error_keys(
  validation : ConfigValidation,
) -> Array[String] {
  validation.issues
  .filter(issue => issue.severity == "error")
  .map(issue => issue.key)
}

///|
pub fn config_environment_is_safe(config : ApplicationConfig) -> Bool {
  let validation = validate_application_config(config)
  validation.valid &&
  config_quality_floor(config) >= 0.50 &&
  config.ingest.maximum_gap_seconds > 0.0 &&
  config.analysis.sample_rate_hz > 0.0
}

///|
pub fn config_for_ci() -> ApplicationConfig {
  let config = application_config_for(ConfigurationBenchmark)
  config
}

///|
pub fn config_for_cli() -> ApplicationConfig {
  let config = application_config_for(ConfigurationProduction)
  config
}

///|
pub fn config_for_tests() -> ApplicationConfig {
  let config = application_config_for(ConfigurationDevelopment)
  config
}

///|
pub fn config_has_valid_windows(config : ApplicationConfig) -> Bool {
  config.analysis.segment_size >= config.analysis.segment_hop &&
  config.analysis.segment_hop > 0 &&
  config.load.chronic_window_days >= config.load.acute_window_days &&
  config.load.acute_window_days > 0
}

///|
pub fn config_has_valid_bounds(config : ApplicationConfig) -> Bool {
  config.ingest.minimum_quality >= 0.0 &&
  config.ingest.minimum_quality <= 1.0 &&
  config.gate.minimum_clean_ratio >= 0.0 &&
  config.gate.minimum_clean_ratio <= 1.0 &&
  config.forecast.lower_bound <= config.forecast.upper_bound
}

///|
pub fn config_is_benchmark_ready(config : ApplicationConfig) -> Bool {
  config.environment == ConfigurationBenchmark &&
  config.deterministic &&
  config_has_valid_windows(config) &&
  config_has_valid_bounds(config)
}

///|
pub fn config_is_production_ready(config : ApplicationConfig) -> Bool {
  config.environment == ConfigurationProduction &&
  config_environment_is_safe(config) &&
  config.subject_retention_days > 0
}

///|
pub fn config_quality_policy_line(config : ApplicationConfig) -> String {
  "\{config.gate.minimum_samples.to_string()} samples, clean=\{config.gate.minimum_clean_ratio.to_string()}, signal=\{config.gate.minimum_signal_quality.to_string()}"
}

///|
pub fn config_ingest_policy_line(config : ApplicationConfig) -> String {
  "gap=\{config.ingest.maximum_gap_seconds.to_string()}s, duration=\{config.ingest.maximum_duration_seconds.to_string()}s, source=\{config.ingest.source_name}"
}

///|
pub fn config_load_policy_line(config : ApplicationConfig) -> String {
  "acute=\{config.load.acute_window_days.to_string()}d, chronic=\{config.load.chronic_window_days.to_string()}d, caution=\{config.load.caution_ratio.to_string()}"
}

///|
pub fn config_forecast_policy_line(config : ApplicationConfig) -> String {
  "horizon=\{config.forecast.horizon.to_string()}, history=\{config.forecast.minimum_history.to_string()}, alpha=\{config.forecast.ewma_alpha.to_string()}"
}

///|
pub fn config_policy_lines(config : ApplicationConfig) -> Array[String] {
  [
    config_summary(config),
    config_quality_policy_line(config),
    config_ingest_policy_line(config),
    config_load_policy_line(config),
    config_forecast_policy_line(config),
    if config_requires_frequency(config) {
      "frequency stage required"
    } else {
      "frequency stage optional"
    },
    if config_requires_nonlinear(config) {
      "nonlinear stage required"
    } else {
      "nonlinear stage optional"
    },
  ]
}

///|
pub fn config_environment_line(config : ApplicationConfig) -> String {
  configuration_environment_name(config.environment)
}

///|
pub fn config_retention_days(config : ApplicationConfig) -> Int {
  config.subject_retention_days
}

///|
pub fn config_quality_floor_is_safe(config : ApplicationConfig) -> Bool {
  config_quality_floor(config) >= 0.50 && config_quality_floor(config) <= 1.0
}

///|
pub fn config_analysis_window_is_safe(config : ApplicationConfig) -> Bool {
  config.analysis.segment_size >= 16 &&
  config.analysis.segment_hop >= 1 &&
  config.analysis.segment_hop <= config.analysis.segment_size
}

///|
pub fn config_allows_streaming(config : ApplicationConfig) -> Bool {
  config.ingest.maximum_duration_seconds > 0.0 &&
  config.ingest.maximum_gap_seconds > 0.0 &&
  config.ingest.source_name.length() > 0
}

///|
pub fn config_readiness_line(config : ApplicationConfig) -> String {
  let validation = validate_application_config(config)
  if !validation.valid {
    config_validation_message(validation)
  } else if config_is_benchmark_ready(config) {
    "configuration is benchmark-ready"
  } else if config_is_production_ready(config) {
    "configuration is production-ready"
  } else {
    "configuration is valid for exploratory use"
  }
}

///|
pub fn config_issue_count(config : ApplicationConfig) -> Int {
  validate_application_config(config).issues.length()
}

///|
pub fn config_is_valid(config : ApplicationConfig) -> Bool {
  validate_application_config(config).valid
}

///|
pub fn config_is_strictly_valid(config : ApplicationConfig) -> Bool {
  config_is_valid(config) &&
  config_has_valid_windows(config) &&
  config_has_valid_bounds(config)
}

///|
pub fn config_status(config : ApplicationConfig) -> String {
  if config_is_strictly_valid(config) {
    "valid"
  } else {
    "invalid"
  }
}