///|
pub fn validate_playlist(playlist : HlsPlaylist) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  validate_shape(playlist, issues)
  validate_media_playlist(playlist, issues)
  validate_master_playlist(playlist, issues)
  validate_uri_audit(playlist, issues)
  report_from_issues(issues)
}

///|
fn validate_shape(playlist : HlsPlaylist, issues : Array[ValidationIssue]) -> Unit {
  if playlist.kind == Empty {
    issues.push(
      ValidationIssue::error(
        "playlist-empty",
        "playlist has no media segments, variants, or renditions",
      ),
    )
  }
  if playlist.kind == Hybrid {
    issues.push(
      ValidationIssue::warning(
        "playlist-hybrid",
        "playlist mixes media segments with master playlist entries",
      ),
    )
  }
  if playlist.version < 0 {
    issues.push(
      ValidationIssue::error(
        "version-invalid",
        "EXT-X-VERSION must be non-negative",
      ),
    )
  }
}

///|
fn validate_media_playlist(
  playlist : HlsPlaylist,
  issues : Array[ValidationIssue],
) -> Unit {
  if playlist.segments.length() == 0 {
    return
  }
  if playlist.target_duration <= 0 {
    issues.push(
      ValidationIssue::error(
        "target-duration-missing",
        "media playlists should declare EXT-X-TARGETDURATION",
      ),
    )
  }
  let mut max_duration = 0
  let mut encrypted = 0
  let mut last_uri = ""
  for segment in playlist.segments {
    if segment.uri == "" {
      issues.push(
        ValidationIssue::error(
          "segment-uri-empty",
          "segment URI is empty",
          line=segment.line,
        ),
      )
    }
    if segment.uri == last_uri {
      issues.push(
        ValidationIssue::warning(
          "segment-uri-duplicate-adjacent",
          "adjacent media segments use the same URI",
          line=segment.line,
        ),
      )
    }
    last_uri = segment.uri
    if segment.duration_ms <= 0 {
      issues.push(
        ValidationIssue::error(
          "segment-duration-invalid",
          "segment duration must be positive",
          line=segment.line,
        ),
      )
    }
    if segment.duration_ms > max_duration {
      max_duration = segment.duration_ms
    }
    match segment.byte_range {
      Some(range) => validate_byte_range(range, segment.line, issues)
      None => ()
    }
    match segment.key {
      Some(key) => {
        if key.key_method != "NONE" {
          encrypted += 1
        }
        validate_key(key, issues)
      }
      None => ()
    }
    match segment.map {
      Some(map) =>
        match map.byte_range {
          Some(range) => validate_byte_range(range, map.line, issues)
          None => ()
        }
      None => ()
    }
  }
  if playlist.target_duration > 0 &&
    max_duration > playlist.target_duration * 1000 {
    issues.push(
      ValidationIssue::error(
        "target-duration-too-small",
        "largest segment duration \{millis_to_string(max_duration)} exceeds EXT-X-TARGETDURATION \{playlist.target_duration}",
      ),
    )
  }
  if encrypted > 0 && playlist.version > 0 && playlist.version < 2 {
    issues.push(
      ValidationIssue::warning(
        "version-encryption-low",
        "encrypted playlists normally declare EXT-X-VERSION 2 or newer",
      ),
    )
  }
}

///|
fn validate_master_playlist(
  playlist : HlsPlaylist,
  issues : Array[ValidationIssue],
) -> Unit {
  for variant in playlist.variants {
    if variant.uri == "" {
      issues.push(
        ValidationIssue::error(
          "variant-uri-empty",
          "variant URI is empty",
          line=variant.line,
        ),
      )
    }
    if variant.bandwidth <= 0 {
      issues.push(
        ValidationIssue::error(
          "variant-bandwidth-invalid",
          "variant BANDWIDTH must be positive",
          line=variant.line,
        ),
      )
    }
    match variant.average_bandwidth {
      Some(value) =>
        if value <= 0 {
          issues.push(
            ValidationIssue::error(
              "variant-average-bandwidth-invalid",
              "AVERAGE-BANDWIDTH must be positive",
              line=variant.line,
            ),
          )
        }
      None => ()
    }
    match variant.resolution {
      Some(resolution) =>
        if resolution.width <= 0 || resolution.height <= 0 {
          issues.push(
            ValidationIssue::error(
              "variant-resolution-invalid",
              "RESOLUTION width and height must be positive",
              line=variant.line,
            ),
          )
        }
      None => ()
    }
    match variant.audio_group {
      Some(group_id) =>
        if !rendition_group_exists(playlist.renditions, "AUDIO", group_id) {
          issues.push(
            ValidationIssue::warning(
              "variant-audio-group-missing",
              "variant references an AUDIO group with no matching EXT-X-MEDIA",
              line=variant.line,
            ),
          )
        }
      None => ()
    }
    match variant.subtitles_group {
      Some(group_id) =>
        if !rendition_group_exists(playlist.renditions, "SUBTITLES", group_id) {
          issues.push(
            ValidationIssue::warning(
              "variant-subtitles-group-missing",
              "variant references a SUBTITLES group with no matching EXT-X-MEDIA",
              line=variant.line,
            ),
          )
        }
      None => ()
    }
  }
  for rendition in playlist.renditions {
    if rendition.rendition_type == "" ||
      rendition.group_id == "" ||
      rendition.name == "" {
      issues.push(
        ValidationIssue::error(
          "rendition-required-empty",
          "rendition TYPE, GROUP-ID, and NAME must be present",
          line=rendition.line,
        ),
      )
    }
    if rendition.forced && rendition.rendition_type != "SUBTITLES" {
      issues.push(
        ValidationIssue::warning(
          "rendition-forced-non-subtitles",
          "FORCED is normally meaningful for subtitle renditions",
          line=rendition.line,
        ),
      )
    }
  }
}

///|
fn rendition_group_exists(
  renditions : Array[Rendition],
  rendition_type : String,
  group_id : String,
) -> Bool {
  for rendition in renditions {
    if rendition.rendition_type == rendition_type && rendition.group_id == group_id {
      return true
    }
  }
  false
}

///|
fn validate_uri_audit(playlist : HlsPlaylist, issues : Array[ValidationIssue]) -> Unit {
  let audit = audit_uris(playlist)
  if audit.empty_uris > 0 {
    issues.push(
      ValidationIssue::error(
        "uri-empty",
        "playlist contains one or more empty URIs",
      ),
    )
  }
  if audit.parent_traversal_uris > 0 {
    issues.push(
      ValidationIssue::warning(
        "uri-parent-traversal",
        "playlist contains URIs with parent directory traversal",
      ),
    )
  }
}

///|
fn validate_key(key : KeyInfo, issues : Array[ValidationIssue]) -> Unit {
  if key.key_method == "" {
    issues.push(
      ValidationIssue::error(
        "key-method-empty",
        "EXT-X-KEY METHOD is empty",
        line=key.line,
      ),
    )
  }
  if key.key_method != "NONE" && key.uri == None {
    issues.push(
      ValidationIssue::error(
        "key-uri-missing",
        "EXT-X-KEY with a non-NONE METHOD should include URI",
        line=key.line,
      ),
    )
  }
}

///|
fn validate_byte_range(
  range : ByteRange,
  line : Int,
  issues : Array[ValidationIssue],
) -> Unit {
  if range.length <= 0 {
    issues.push(
      ValidationIssue::error(
        "byte-range-length-invalid",
        "byte range length must be positive",
        line~,
      ),
    )
  }
  match range.offset {
    Some(offset) =>
      if offset < 0 {
        issues.push(
          ValidationIssue::error(
            "byte-range-offset-invalid",
            "byte range offset must be non-negative",
            line~,
          ),
        )
      }
    None => ()
  }
}

///|
fn report_from_issues(issues : Array[ValidationIssue]) -> ValidationReport {
  let mut errors = 0
  let mut warnings = 0
  let mut infos = 0
  for issue in issues {
    match issue.severity {
      Error => errors += 1
      Warning => warnings += 1
      Info => infos += 1
    }
  }
  { ok: errors == 0, errors, warnings, infos, issues }
}