///|
pub(all) enum PageKind {
  BtreeRoot
  BtreeChild
  OverflowFirst
  OverflowContinuation
  FreelistTrunk
  FreelistLeaf
  PointerMap
  LockByte
} derive(Debug, Eq)

///|
pub extend PageKind with @debug.Debug::{to_repr}

///|
pub extend PageKind with Eq::{equal, not_equal}

///|
pub(all) struct PageOwnership {
  page_number : Int
  kind : PageKind
  root_page : Int?
  object_name : String?
  parent_page : Int?
} derive(Debug, Eq)

///|
pub extend PageOwnership with @debug.Debug::{to_repr}

///|
pub extend PageOwnership with Eq::{equal, not_equal}

///|
pub(all) enum DatabaseIssue {
  ScanError(Int?, SqliteError)
  PageConflict(PageOwnership, PageOwnership)
  UnclaimedPage(Int)
  UnsupportedFeature(String)
  PtrmapMismatch(PtrmapEntry, PageOwnership)
} derive(Debug)

///|
pub extend DatabaseIssue with @debug.Debug::{to_repr}

///|
/// 只检查逻辑数据库页;完整页归属不等价于 SQL 完整性检查。
pub(all) struct DatabaseInspection {
  status : InspectionStatus
  ownership_complete : Bool
  ptrmap_checked : Bool
  diagnostics_truncated : Bool
  roots_inspected : Int
  records_decoded : Int
  payload_bytes : UInt64
  pages : Array[PageOwnership]
  unclaimed_pages : Array[Int]
  issues : Array[DatabaseIssue]
} derive(Debug)

///|
pub extend DatabaseInspection with @debug.Debug::{to_repr}

///|
priv struct OwnershipState {
  owners : Map[Int, PageOwnership]
  issues : Array[DatabaseIssue]
  max_pages : Int
  max_issues : Int
  mut coverage : Bool
  mut failed : Bool
  mut truncated : Bool
  mut roots : Int
  mut records : Int
  mut payload : UInt64
}

///|
fn OwnershipState::issue(self : OwnershipState, issue : DatabaseIssue) -> Unit {
  if self.issues.length() < self.max_issues {
    self.issues.push(issue)
  } else {
    self.truncated = true
  }
}

///|
fn OwnershipState::error(
  self : OwnershipState,
  root : Int?,
  error : SqliteError,
) -> Unit {
  self.coverage = false
  if error is Invalid(_) {
    self.failed = true
  }
  self.issue(ScanError(root, error))
}

///|
fn OwnershipState::claim(
  self : OwnershipState,
  owner : PageOwnership,
) -> Unit raise SqliteError {
  if self.owners.get(owner.page_number) is Some(previous) {
    self.failed = true
    self.issue(PageConflict(previous, owner))
    raise Invalid("页 \{owner.page_number} 被多个用途或对象占用")
  }
  if self.owners.length() >= self.max_pages {
    raise LimitExceeded("数据库级页数预算已耗尽")
  }
  self.owners[owner.page_number] = owner
}

///|
fn OwnershipState::observer(
  self : OwnershipState,
  root : Int,
  name : String,
) -> ScanObserver {
  {
    claim: (number, kind, parent) => {
      self.claim({
        page_number: number,
        kind,
        root_page: Some(root),
        object_name: Some(name),
        parent_page: parent,
      })
    },
    charge_payload: size => self.payload = self.payload + size,
  }
}

///|
fn OwnershipState::finish(
  self : OwnershipState,
  page_count : Int,
  ptrmap_checked : Bool,
) -> DatabaseInspection {
  let pages : Array[PageOwnership] = []
  let unclaimed : Array[Int] = []
  // 预检拒绝超预算的数据库,避免枚举或分配无界的页列表。
  if page_count <= self.max_pages {
    for number = 1; number <= page_count; number = number + 1 {
      if self.owners.get(number) is Some(owner) {
        pages.push(owner)
      } else {
        unclaimed.push(number)
        if self.coverage {
          self.failed = true
          self.issue(UnclaimedPage(number))
        }
      }
    }
  }
  let status = if self.failed {
    Failed
  } else if !self.coverage || !ptrmap_checked || self.truncated {
    Incomplete
  } else {
    Complete
  }
  {
    status,
    ownership_complete: self.coverage,
    ptrmap_checked,
    diagnostics_truncated: self.truncated,
    roots_inspected: self.roots,
    records_decoded: self.records,
    payload_bytes: self.payload,
    pages,
    unclaimed_pages: unclaimed,
    issues: self.issues,
  }
}

///|
/// 扫描 schema、独立 B-tree 和 freelist,使用整个检查过程共享的预算。
pub fn Database::inspect_database(
  self : Database,
  max_total_payload_bytes? : UInt64 = 67108864UL,
  max_issues? : Int = 100,
) -> DatabaseInspection {
  let state : OwnershipState = {
    owners: Map([]),
    issues: [],
    max_pages: self.limits.max_pages,
    max_issues: if max_issues > 0 {
      max_issues
    } else {
      1
    },
    coverage: true,
    failed: false,
    truncated: false,
    roots: 0,
    records: 0,
    payload: 0UL,
  }
  let vacuum = self.header.largest_root_page != 0U
  if max_issues < 1 {
    state.error(None, Invalid("诊断条数上限必须为正数"))
    return state.finish(0, !vacuum)
  }
  if self.page_count > self.limits.max_pages {
    state.error(
      None,
      LimitExceeded("数据库逻辑页数超过全局检查预算"),
    )
    return state.finish(self.page_count, false)
  }
  if self.header.largest_root_page.to_uint64() > self.page_count.to_uint64() ||
    (!vacuum && self.header.incremental_vacuum) {
    state.error(None, Invalid("auto-vacuum 文件头字段不一致"))
    return state.finish(self.page_count, false)
  }
  let lock_page = 1073741824 / self.header.page_size + 1
  if lock_page <= self.page_count {
    // 这些保留页仅按位置认领,不执行 B-tree 解码。
    state.claim({
      page_number: lock_page,
      kind: LockByte,
      root_page: None,
      object_name: None,
      parent_page: None,
    }) catch {
      Invalid(message) => state.error(None, Invalid(message))
      LimitExceeded(message) => state.error(None, LimitExceeded(message))
      Unsupported(message) => state.error(None, Unsupported(message))
    }
  }
  if vacuum {
    let step = self.header.usable_size / 5 + 1
    let mut number = 2
    while number <= self.page_count {
      let actual = if number == lock_page { number + 1 } else { number }
      if actual <= self.page_count {
        state.claim({
          page_number: actual,
          kind: PointerMap,
          root_page: None,
          object_name: None,
          parent_page: None,
        }) catch {
          Invalid(message) => state.error(None, Invalid(message))
          LimitExceeded(message) => state.error(None, LimitExceeded(message))
          Unsupported(message) => state.error(None, Unsupported(message))
        }
      }
      number = number + step
    }
  }
  let entries : Array[SchemaEntry] = []
  state.roots = state.roots + 1
  let schema_ok = try {
    let kind = self.page(1).kind
    if kind != TableLeaf && kind != TableInterior {
      raise Invalid("sqlite_schema 根页必须是 table B-tree")
    }
    let summary = self.scan_btree_observed(
      1,
      record => {
        state.records = state.records + 1
        entries.push(schema_entry(record.values, self.page_count))
        true
      },
      state.observer(1, "sqlite_schema"),
      limit=self.limits.max_rows,
      max_total_payload_bytes~,
    )
    if summary.completion != Complete {
      state.error(
        Some(1),
        LimitExceeded("schema 未完整读取,无法发现全部根页"),
      )
      false
    } else {
      true
    }
  } catch {
    Invalid(message) => {
      state.error(Some(1), Invalid(message))
      false
    }
    Unsupported(message) => {
      state.error(Some(1), Unsupported(message))
      false
    }
    LimitExceeded(message) => {
      state.error(Some(1), LimitExceeded(message))
      false
    }
  }
  if !schema_ok {
    return state.finish(self.page_count, !vacuum)
  }
  for entry in entries {
    if state.issues.length() >= state.max_issues {
      state.coverage = false
      state.truncated = true
      break
    }
    if entry.object_type == "view" || entry.object_type == "trigger" {
      if entry.root_page != 0 {
        state.error(
          Some(entry.root_page),
          Invalid("view 或 trigger 不能拥有根页"),
        )
      }
      continue
    }
    if entry.root_page == 0 {
      if entry.object_type == "index" {
        state.error(Some(0), Invalid("索引没有根页"))
      }
      // 虚拟表定义不占独立 B-tree,shadow 表仍按 schema 独立扫描。
      continue
    }
    state.roots = state.roots + 1
    try {
      if entry.object_type == "index" {
        let kind = self.page(entry.root_page).kind
        if kind != IndexLeaf && kind != IndexInterior {
          raise Invalid("schema 索引根页不是 index B-tree")
        }
      }
      let summary = self.scan_btree_observed(
        entry.root_page,
        _ => {
          state.records = state.records + 1
          true
        },
        state.observer(entry.root_page, entry.name),
        limit=self.limits.max_rows - state.records,
        max_total_payload_bytes=max_total_payload_bytes - state.payload,
      )
      if summary.completion != Complete {
        state.error(
          Some(entry.root_page),
          LimitExceeded("全局记录数预算已耗尽"),
        )
      }
    } catch {
      Invalid(message) => state.error(Some(entry.root_page), Invalid(message))
      Unsupported(message) =>
        state.error(Some(entry.root_page), Unsupported(message))
      LimitExceeded(message) =>
        state.error(Some(entry.root_page), LimitExceeded(message))
    }
  }
  if state.issues.length() < state.max_issues {
    ignore(
      self.freelist_observed((number, kind, parent) => {
        state.claim({
          page_number: number,
          kind,
          root_page: None,
          object_name: None,
          parent_page: parent,
        })
      }),
    ) catch {
      Invalid(message) => state.error(None, Invalid(message))
      Unsupported(message) => state.error(None, Unsupported(message))
      LimitExceeded(message) => state.error(None, LimitExceeded(message))
    }
  } else {
    state.coverage = false
    state.truncated = true
  }
  let ptrmap_checked = state.check_ptrmap(self)
  state.finish(self.page_count, ptrmap_checked)
}