///|
/// Dependency-free tabular adapters for importing keymaps from CI artifacts.
pub(all) struct BindingRow {
  id : String
  command : String
  keys : String
  context : String
  platform : String
  priority : String
  enabled : String
  description : String
} derive(Eq, @debug.Debug)

///|
pub(all) struct ImportReport {
  keymap : Keymap
  rows_seen : Int
  rows_imported : Int
  diagnostics : Array[ParseDiagnostic]
} derive(Eq, @debug.Debug)

///|
fn csv_field(value : String) -> String {
  let needs_quote = value.contains(",") ||
    value.contains("\"") ||
    value.contains("\n")
  if needs_quote {
    "\"" + value.replace(old="\"", new="\"\"") + "\""
  } else {
    value
  }
}

///|
fn parse_csv_line(line : String) -> Array[String] {
  let result : Array[String] = []
  let mut current = StringBuilder()
  let mut quoted = false
  let mut i = 0
  while i < line.length() {
    let code = line.unsafe_get(i).to_int()
    if code == 34 {
      if quoted &&
        i + 1 < line.length() &&
        line.unsafe_get(i + 1).to_int() == 34 {
        current.write_char('"')
        i += 2
        continue
      }
      quoted = !quoted
    } else if code == 44 && !quoted {
      result.push(current.to_string())
      current = StringBuilder()
    } else {
      current.write_char(line.unsafe_get(i).to_int().unsafe_to_char())
    }
    i += 1
  }
  result.push(current.to_string())
  result
}

///|
fn csv_header_index(headers : Array[String], wanted : String) -> Int {
  for i, header in headers {
    if lower_ascii(trim_ascii(header)) == wanted {
      return i
    }
  }
  -1
}

///|
fn csv_value(row : Array[String], index : Int, default : String) -> String {
  if index >= 0 && index < row.length() {
    trim_ascii(row[index])
  } else {
    default
  }
}

///|
/// Import the stable CSV schema emitted by `export_csv`.
pub fn import_csv(
  source : String,
  name? : String = "csv-keymap",
) -> ImportReport {
  let lines = source.split("\n").map(x => x.to_owned()).to_array()
  let diagnostics : Array[ParseDiagnostic] = []
  if lines.length() == 0 {
    return {
      keymap: Keymap::empty(name~),
      rows_seen: 0,
      rows_imported: 0,
      diagnostics: [
        {
          line: 1,
          code: "empty-csv",
          message: "CSV document is empty",
          source: "",
        },
      ],
    }
  }
  let headers = parse_csv_line(lines[0])
  let id_index = csv_header_index(headers, "id")
  let command_index = csv_header_index(headers, "command")
  let keys_index = csv_header_index(headers, "keys")
  if id_index < 0 || command_index < 0 || keys_index < 0 {
    diagnostics.push({
      line: 1,
      code: "missing-csv-columns",
      message: "CSV requires id, command, and keys columns",
      source: "header",
    })
  }
  let keymap = Keymap::empty(name~)
  let mut imported = 0
  for line_index in 1..
        diagnostics.push({
          line: line_index + 1,
          code: "invalid-csv-keys",
          message,
          source: id,
        })
      Ok(keys) => {
        let context = csv_value(
          row,
          csv_header_index(headers, "context"),
          "global",
        )
        let platform = csv_value(
          row,
          csv_header_index(headers, "platform"),
          "all",
        )
        let priority = int_value(
          csv_value(row, csv_header_index(headers, "priority"), "0"),
          0,
        )
        let enabled = bool_value(
          csv_value(row, csv_header_index(headers, "enabled"), "true"),
          true,
        )
        let description = csv_value(
          row,
          csv_header_index(headers, "description"),
          "",
        )
        if context != "global" && !context_exists(keymap, context) {
          keymap.contexts.push({
            name: context,
            parent: "global",
            rank: 1,
            description: "imported",
          })
        }
        keymap.bindings.push(
          Binding::new(
            id,
            command,
            keys,
            context~,
            platform~,
            source="csv",
            line=line_index + 1,
            priority~,
            enabled~,
            description~,
          ),
        )
        imported += 1
      }
    }
  }
  {
    keymap,
    rows_seen: if lines.length() > 0 {
      lines.length() - 1
    } else {
      0
    },
    rows_imported: imported,
    diagnostics,
  }
}

///|
/// Import tab-separated rows using the same column names as CSV.
pub fn import_tsv(
  source : String,
  name? : String = "tsv-keymap",
) -> ImportReport {
  import_csv(source.replace(old="\t", new=","), name~)
}

///|
/// Export a keymap using a stable, documented column order.
pub fn export_csv(keymap : Keymap) -> String {
  let lines : Array[String] = [
    "id,command,keys,context,platform,priority,enabled,description",
  ]
  for binding in normalize_keymap(keymap).bindings {
    let enabled = if binding.enabled { "true" } else { "false" }
    lines.push(
      [
        csv_field(binding.id),
        csv_field(binding.command),
        csv_field(binding.keys.canonical),
        csv_field(binding.context),
        csv_field(binding.platform),
        binding.priority.to_string(),
        enabled,
        csv_field(binding.description),
      ].join(","),
    )
  }
  lines.join("\n")
}

///|
/// A compact row constructor for integrations that already have columns.
pub fn row(
  id : String,
  command : String,
  keys : String,
  context? : String = "global",
  platform? : String = "all",
  priority? : String = "0",
  enabled? : String = "true",
  description? : String = "",
) -> BindingRow {
  { id, command, keys, context, platform, priority, enabled, description }
}

///|
/// Construct a keymap from rows and preserve row-level diagnostics.
pub fn import_rows(
  rows : Array[BindingRow],
  name? : String = "row-keymap",
) -> ImportReport {
  let keymap = Keymap::empty(name~)
  let diagnostics : Array[ParseDiagnostic] = []
  let mut imported = 0
  for index, item in rows {
    match parse_keys(item.keys) {
      Err(message) =>
        diagnostics.push({
          line: index + 1,
          code: "invalid-row-keys",
          message,
          source: item.id,
        })
      Ok(keys) => {
        let context = string_or_default(item.context, "global")
        if context != "global" && !context_exists(keymap, context) {
          keymap.contexts.push({
            name: context,
            parent: "global",
            rank: 1,
            description: "row-import",
          })
        }
        keymap.bindings.push(
          Binding::new(
            item.id,
            item.command,
            keys,
            context~,
            platform=string_or_default(item.platform, "all"),
            source="row",
            line=index + 1,
            priority=int_value(item.priority, 0),
            enabled=bool_value(item.enabled, true),
            description=item.description,
          ),
        )
        imported += 1
      }
    }
  }
  { keymap, rows_seen: rows.length(), rows_imported: imported, diagnostics }
}

///|
fn vscode_context(when : String) -> String {
  let clean = lower_ascii(trim_ascii(when))
  if clean.length() == 0 || clean == "editortextfocus" || clean == "editorfocus" {
    "editor"
  } else {
    "global"
  }
}

///|
/// Import a deliberately small VS Code-style line format:
/// `command | key | when | platform`.
pub fn import_pipe(
  source : String,
  name? : String = "pipe-keymap",
) -> ImportReport {
  let rows : Array[BindingRow] = []
  let lines = source.split("\n").map(x => x.to_owned()).to_array()
  for line in lines {
    let clean = trim_ascii(line)
    if clean.length() == 0 || clean[0] == '#' {
      continue
    }
    let fields = clean.split("|").map(x => trim_ascii(x.to_owned())).to_array()
    if fields.length() >= 2 {
      let command = fields[0]
      let keys = fields[1]
      let context = if fields.length() > 2 {
        vscode_context(fields[2])
      } else {
        "global"
      }
      let platform = if fields.length() > 3 { fields[3] } else { "all" }
      rows.push(
        row(
          command,
          command,
          keys,
          context~,
          platform~,
          description="pipe import",
        ),
      )
    }
  }
  import_rows(rows, name~)
}

///|
pub fn import_report_to_text(report : ImportReport) -> String {
  let lines : Array[String] = [
    "imported " +
    report.rows_imported.to_string() +
    "/" +
    report.rows_seen.to_string() +
    " rows",
    "keymap=" +
    report.keymap.name +
    " bindings=" +
    report.keymap.bindings.length().to_string(),
  ]
  for diagnostic in report.diagnostics {
    lines.push(
      "line " +
      diagnostic.line.to_string() +
      " " +
      diagnostic.code +
      ": " +
      diagnostic.message,
    )
  }
  lines.join("\n")
}