/// <reference path="../types/fresh.d.ts" />

/**
 * Config Editor Plugin - Schema-driven configuration editor
 *
 * Provides an interactive UI for editing Fresh's config file with:
 * - Automatic adaptation to the full JSON schema (loaded from config-schema.json)
 * - Type-appropriate widgets (toggles for booleans, dropdowns for enums, etc.)
 * - Support for "keep default" vs custom values
 * - Nested object navigation
 *
 * Inspired by Emacs Customize mode
 */

// =============================================================================
// Types and Schema System
// =============================================================================

/**
 * Field types supported by the config editor
 */
type FieldType = "boolean" | "number" | "string" | "enum" | "array" | "object";

/**
 * Schema definition for a config field (internal representation)
 */
interface FieldSchema {
  /** Field type */
  type: FieldType;
  /** Human-readable description */
  description: string;
  /** Default value (undefined means no default/required) */
  defaultValue?: unknown;
  /** For enums: list of valid options */
  enumOptions?: string[];
  /** For numbers: minimum value */
  min?: number;
  /** For numbers: maximum value */
  max?: number;
  /** For arrays: schema of array items */
  itemSchema?: FieldSchema;
  /** For objects: schema of nested fields */
  nestedSchema?: Record<string, FieldSchema>;
  /** Whether this field is optional */
  optional?: boolean;
}

/**
 * JSON Schema format (as generated by schemars)
 */
interface JsonSchema {
  $schema?: string;
  title?: string;
  description?: string;
  type?: string | string[];
  properties?: Record<string, JsonSchema>;
  additionalProperties?: JsonSchema | boolean;
  items?: JsonSchema;
  allOf?: JsonSchema[];
  anyOf?: JsonSchema[];
  oneOf?: JsonSchema[];
  $ref?: string;
  default?: unknown;
  enum?: unknown[];
  minimum?: number;
  maximum?: number;
  format?: string;
  definitions?: Record<string, JsonSchema>;
  $defs?: Record<string, JsonSchema>; // JSON Schema draft-07+ uses $defs
}

/**
 * Represents a displayable config field with its path and value
 */
interface ConfigField {
  /** Dot-separated path (e.g., "editor.tab_size") */
  path: string;
  /** Display name */
  name: string;
  /** Field schema */
  schema: FieldSchema;
  /** Current value */
  value: unknown;
  /** Whether using default value */
  isDefault: boolean;
  /** Indentation level */
  depth: number;
  /** Whether this is a section header */
  isSection: boolean;
  /** Whether section is expanded */
  expanded?: boolean;
}

// =============================================================================
// JSON Schema Loading and Conversion
// =============================================================================

/** Cached loaded schema */
let loadedJsonSchema: JsonSchema | null = null;
let convertedSchema: Record<string, FieldSchema> | null = null;

/**
 * Find the schema file path (relative to plugin location)
 */
function findSchemaPath(): string {
  // The schema is generated alongside plugins
  const cwd = editor.getCwd();

  // Try several locations
  const candidates = [
    editor.pathJoin(cwd, "plugins", "config-schema.json"),
    editor.pathJoin(cwd, "config-schema.json"),
  ];

  // Also try relative to HOME/.config/fresh
  const home = editor.getEnv("HOME");
  if (home) {
    candidates.push(editor.pathJoin(home, ".config", "fresh", "plugins", "config-schema.json"));
  }

  for (const path of candidates) {
    if (editor.fileExists(path)) {
      return path;
    }
  }

  // Return first candidate even if not found (will error on load)
  return candidates[0];
}

/**
 * Load and parse the JSON Schema file
 */
async function loadJsonSchema(): Promise<JsonSchema | null> {
  if (loadedJsonSchema) {
    return loadedJsonSchema;
  }

  const schemaPath = findSchemaPath();
  try {
    const content = await editor.readFile(schemaPath);
    loadedJsonSchema = JSON.parse(content) as JsonSchema;
    editor.debug(`Loaded config schema from ${schemaPath}`);
    return loadedJsonSchema;
  } catch (e) {
    editor.debug(`Failed to load schema from ${schemaPath}: ${e}`);
    return null;
  }
}

/**
 * Resolve a $ref in JSON Schema
 */
function resolveRef(ref: string, rootSchema: JsonSchema): JsonSchema | null {
  // Format: #/definitions/TypeName or #/$defs/TypeName (JSON Schema draft-07+)
  if (ref.startsWith("#/definitions/")) {
    const typeName = ref.substring("#/definitions/".length);
    return rootSchema.definitions?.[typeName] ?? null;
  }
  if (ref.startsWith("#/$defs/")) {
    const typeName = ref.substring("#/$defs/".length);
    return rootSchema.$defs?.[typeName] ?? null;
  }
  return null;
}

/**
 * Convert a JSON Schema property to our internal FieldSchema format
 */
function convertJsonSchemaToFieldSchema(
  jsonSchema: JsonSchema,
  rootSchema: JsonSchema,
  propertyName?: string
): FieldSchema {
  // Handle $ref
  if (jsonSchema.$ref) {
    const resolved = resolveRef(jsonSchema.$ref, rootSchema);
    if (resolved) {
      return convertJsonSchemaToFieldSchema(resolved, rootSchema, propertyName);
    }
  }

  // Handle allOf (common pattern from schemars for $ref + default)
  if (jsonSchema.allOf && jsonSchema.allOf.length > 0) {
    // Merge all schemas in allOf
    let merged = convertJsonSchemaToFieldSchema(jsonSchema.allOf[0], rootSchema, propertyName);
    // Apply default from parent if present
    if (jsonSchema.default !== undefined) {
      merged.defaultValue = jsonSchema.default;
    }
    return merged;
  }

  // Handle anyOf/oneOf (union types - pick the most specific object variant)
  const unionSchemas = jsonSchema.anyOf || jsonSchema.oneOf;
  if (unionSchemas && unionSchemas.length > 0) {
    // Find the most useful variant - prefer objects with properties over primitives
    let bestSchema: JsonSchema | null = null;
    let bestScore = -1;

    for (const variant of unionSchemas) {
      // Resolve any refs
      let resolved = variant;
      if (variant.$ref) {
        const ref = resolveRef(variant.$ref, rootSchema);
        if (ref) resolved = ref;
      }

      // Score based on usefulness for editing
      let score = 0;
      if (resolved.type === "object" && resolved.properties) {
        score = 10 + Object.keys(resolved.properties).length;
      } else if (resolved.type === "object") {
        score = 5;
      } else if (resolved.type === "string" || resolved.type === "number" || resolved.type === "boolean") {
        score = 1;
      }

      if (score > bestScore) {
        bestScore = score;
        bestSchema = resolved;
      }
    }

    if (bestSchema) {
      const result = convertJsonSchemaToFieldSchema(bestSchema, rootSchema, propertyName);
      // Store all variants for potential future use (e.g., variant selection UI)
      result.description = jsonSchema.description || result.description;
      return result;
    }
  }

  // Determine type
  let fieldType: FieldType = "string";
  const jsonType = Array.isArray(jsonSchema.type) ? jsonSchema.type[0] : jsonSchema.type;

  if (jsonSchema.enum) {
    fieldType = "enum";
  } else if (jsonType === "boolean") {
    fieldType = "boolean";
  } else if (jsonType === "integer" || jsonType === "number") {
    fieldType = "number";
  } else if (jsonType === "string") {
    fieldType = "string";
  } else if (jsonType === "array") {
    fieldType = "array";
  } else if (jsonType === "object") {
    fieldType = "object";
  }

  const result: FieldSchema = {
    type: fieldType,
    description: jsonSchema.description || propertyName || "",
    defaultValue: jsonSchema.default,
  };

  // Handle enums
  if (jsonSchema.enum) {
    result.enumOptions = jsonSchema.enum.map(e => String(e));
  }

  // Handle number constraints
  if (jsonSchema.minimum !== undefined) {
    result.min = jsonSchema.minimum;
  }
  if (jsonSchema.maximum !== undefined) {
    result.max = jsonSchema.maximum;
  }

  // Handle arrays
  if (fieldType === "array" && jsonSchema.items) {
    result.itemSchema = convertJsonSchemaToFieldSchema(jsonSchema.items, rootSchema);
  }

  // Handle objects with properties
  if (fieldType === "object" && jsonSchema.properties) {
    result.nestedSchema = {};
    for (const [key, propSchema] of Object.entries(jsonSchema.properties)) {
      result.nestedSchema[key] = convertJsonSchemaToFieldSchema(propSchema, rootSchema, key);
    }
  }

  // Handle additionalProperties (for HashMap types like languages, lsp)
  if (fieldType === "object" && jsonSchema.additionalProperties && typeof jsonSchema.additionalProperties === "object") {
    // This is a map type - store the value schema
    result.itemSchema = convertJsonSchemaToFieldSchema(jsonSchema.additionalProperties, rootSchema);
  }

  return result;
}

/**
 * Convert the full JSON Schema to our internal schema format
 */
function convertFullSchema(jsonSchema: JsonSchema): Record<string, FieldSchema> {
  const result: Record<string, FieldSchema> = {};

  // Resolve root $ref if present (common pattern: root schema points to main type)
  let targetSchema = jsonSchema;
  if (jsonSchema.$ref) {
    const resolved = resolveRef(jsonSchema.$ref, jsonSchema);
    if (resolved) {
      targetSchema = resolved;
    }
  }

  if (!targetSchema.properties) {
    return result;
  }

  for (const [key, propSchema] of Object.entries(targetSchema.properties)) {
    result[key] = convertJsonSchemaToFieldSchema(propSchema, jsonSchema, key);
  }

  return result;
}

/**
 * Get the config schema (loads from file if needed)
 */
async function getConfigSchema(): Promise<Record<string, FieldSchema>> {
  if (convertedSchema) {
    return convertedSchema;
  }

  const jsonSchema = await loadJsonSchema();
  if (jsonSchema) {
    convertedSchema = convertFullSchema(jsonSchema);
    return convertedSchema;
  }

  // Fallback to minimal hardcoded schema if file not found
  editor.debug("Using fallback minimal schema");
  return {
    theme: { type: "string", description: "Color theme name", defaultValue: "high-contrast" },
    editor: { type: "object", description: "Editor settings", nestedSchema: {} },
    file_explorer: { type: "object", description: "File explorer settings", nestedSchema: {} },
    active_keybinding_map: {
      type: "enum",
      description: "Keybinding style",
      defaultValue: "default",
      enumOptions: ["default", "emacs", "vscode"]
    },
  };
}

// =============================================================================
// State Management
// =============================================================================

interface ConfigEditorState {
  isOpen: boolean;
  bufferId: number | null;
  splitId: number | null;
  sourceSplitId: number | null;
  sourceBufferId: number | null;
  /** Original config from file */
  originalConfig: Record<string, unknown>;
  /** Working copy with modifications */
  workingConfig: Record<string, unknown>;
  /** Expanded section paths */
  expandedSections: Set<string>;
  /** Flat list of visible fields */
  visibleFields: ConfigField[];
  /** Current cursor line (0-indexed field index) */
  selectedIndex: number;
  /** Path to config file */
  configPath: string;
  /** Whether there are unsaved changes */
  hasChanges: boolean;
  /** Cached content for highlighting */
  cachedContent: string;
  /** Loaded schema from JSON file */
  schema: Record<string, FieldSchema>;
}

const state: ConfigEditorState = {
  isOpen: false,
  bufferId: null,
  splitId: null,
  sourceSplitId: null,
  sourceBufferId: null,
  originalConfig: {},
  workingConfig: {},
  expandedSections: new Set(["editor", "file_explorer"]),
  visibleFields: [],
  selectedIndex: 0,
  configPath: "",
  hasChanges: false,
  cachedContent: "",
  schema: {},
};

// =============================================================================
// Color Definitions
// =============================================================================

const colors = {
  sectionHeader: [255, 200, 100] as [number, number, number],   // Gold
  fieldName: [200, 200, 255] as [number, number, number],       // Light blue
  defaultValue: [150, 150, 150] as [number, number, number],    // Gray (dimmed)
  customValue: [100, 255, 100] as [number, number, number],     // Green
  boolTrue: [100, 255, 150] as [number, number, number],        // Green
  boolFalse: [255, 150, 150] as [number, number, number],       // Red
  enumValue: [200, 150, 255] as [number, number, number],       // Purple
  numberValue: [255, 200, 150] as [number, number, number],     // Orange
  stringValue: [150, 220, 150] as [number, number, number],     // Light green
  selected: [60, 60, 100] as [number, number, number],          // Selection bg
  description: [120, 120, 120] as [number, number, number],     // Dim gray
  modified: [255, 255, 100] as [number, number, number],        // Yellow
  footer: [100, 100, 100] as [number, number, number],          // Gray
};

// =============================================================================
// Mode Definition
// =============================================================================

editor.defineMode(
  "config-editor",
  "normal",
  [
    ["Return", "config_editor_edit_field"],
    ["Space", "config_editor_toggle_or_edit"],
    ["Tab", "config_editor_toggle_section"],
    ["d", "config_editor_reset_to_default"],
    ["s", "config_editor_save"],
    ["q", "config_editor_close"],
    ["Escape", "config_editor_close"],
    ["r", "config_editor_reload"],
    ["?", "config_editor_show_help"],
  ],
  true // read-only
);

// =============================================================================
// Utility Functions
// =============================================================================

/**
 * Parse a path string into parts, handling both dot notation and array indices
 * e.g., "keybindings[0].key" -> ["keybindings", "0", "key"]
 */
function parsePath(path: string): string[] {
  const parts: string[] = [];
  let current = "";
  for (let i = 0; i < path.length; i++) {
    const char = path[i];
    if (char === ".") {
      if (current) parts.push(current);
      current = "";
    } else if (char === "[") {
      if (current) parts.push(current);
      current = "";
    } else if (char === "]") {
      if (current) parts.push(current);
      current = "";
    } else {
      current += char;
    }
  }
  if (current) parts.push(current);
  return parts;
}

/**
 * Get a nested value from an object using a path (supports array indices)
 */
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
  const parts = parsePath(path);
  let current: unknown = obj;
  for (const part of parts) {
    if (current === null || current === undefined) return undefined;
    if (Array.isArray(current)) {
      const index = parseInt(part, 10);
      if (isNaN(index)) return undefined;
      current = current[index];
    } else if (typeof current === "object") {
      current = (current as Record<string, unknown>)[part];
    } else {
      return undefined;
    }
  }
  return current;
}

/**
 * Set a nested value in an object using a path (supports array indices)
 */
function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
  const parts = parsePath(path);
  let current: unknown = obj;
  for (let i = 0; i < parts.length - 1; i++) {
    const part = parts[i];
    const nextPart = parts[i + 1];
    const nextIsArrayIndex = /^\d+$/.test(nextPart);

    if (Array.isArray(current)) {
      const index = parseInt(part, 10);
      if (current[index] === undefined || current[index] === null) {
        current[index] = nextIsArrayIndex ? [] : {};
      }
      current = current[index];
    } else {
      const record = current as Record<string, unknown>;
      if (!(part in record) || record[part] === null || record[part] === undefined) {
        record[part] = nextIsArrayIndex ? [] : {};
      }
      current = record[part];
    }
  }
  const lastPart = parts[parts.length - 1];
  if (Array.isArray(current)) {
    const index = parseInt(lastPart, 10);
    (current as unknown[])[index] = value;
  } else {
    (current as Record<string, unknown>)[lastPart] = value;
  }
}

/**
 * Delete a nested value from an object (supports array indices)
 */
function deleteNestedValue(obj: Record<string, unknown>, path: string): void {
  const parts = parsePath(path);
  let current: unknown = obj;
  for (let i = 0; i < parts.length - 1; i++) {
    const part = parts[i];
    if (Array.isArray(current)) {
      const index = parseInt(part, 10);
      if (isNaN(index) || current[index] === undefined) return;
      current = current[index];
    } else if (typeof current === "object" && current !== null) {
      if (!(part in (current as Record<string, unknown>))) return;
      current = (current as Record<string, unknown>)[part];
    } else {
      return;
    }
  }
  const lastPart = parts[parts.length - 1];
  if (Array.isArray(current)) {
    const index = parseInt(lastPart, 10);
    if (!isNaN(index)) {
      (current as unknown[]).splice(index, 1);
    }
  } else if (typeof current === "object" && current !== null) {
    delete (current as Record<string, unknown>)[lastPart];
  }
}

/**
 * Infer a schema from an actual object's structure
 * Used when JSON Schema doesn't provide explicit nestedSchema (e.g., anyOf/oneOf patterns)
 *
 * TODO: schemars generates broken schemas for Rust #[serde(untagged)] enums (like MenuItem).
 * Instead of proper anyOf with object variants, it generates a string enum with field descriptions.
 * Options to fix:
 *   1. Custom schema post-processing in build.rs to detect and fix untagged enum patterns
 *   2. Change MenuItem etc. to use tagged enums (changes JSON format but gives proper schemas)
 *   3. Use a different schema generator that handles untagged enums better
 * Option #2 is likely the cleanest - tagged enums are more explicit and self-documenting.
 */
function inferSchemaFromObject(obj: Record<string, unknown>): Record<string, FieldSchema> {
  const schema: Record<string, FieldSchema> = {};
  for (const [key, value] of Object.entries(obj)) {
    schema[key] = inferSchemaFromValue(value, key);
  }
  return schema;
}

/**
 * Infer a field schema from a value's type
 */
function inferSchemaFromValue(value: unknown, name: string): FieldSchema {
  if (value === null || value === undefined) {
    return { type: "string", description: name };
  }
  if (typeof value === "boolean") {
    return { type: "boolean", description: name };
  }
  if (typeof value === "number") {
    return { type: "number", description: name };
  }
  if (typeof value === "string") {
    return { type: "string", description: name };
  }
  if (Array.isArray(value)) {
    const itemSchema = value.length > 0 ? inferSchemaFromValue(value[0], "item") : { type: "string" as FieldType, description: "item" };
    return { type: "array", description: name, itemSchema };
  }
  if (typeof value === "object") {
    return {
      type: "object",
      description: name,
      nestedSchema: inferSchemaFromObject(value as Record<string, unknown>),
    };
  }
  return { type: "string", description: name };
}

/**
 * Calculate UTF-8 byte length of a string without using TextEncoder
 * (TextEncoder is not available in Deno plugin runtime)
 */
function getUtf8ByteLength(str: string): number {
  let length = 0;
  for (let i = 0; i < str.length; i++) {
    const code = str.charCodeAt(i);
    if (code < 0x80) {
      length += 1;
    } else if (code < 0x800) {
      length += 2;
    } else if (code < 0xD800 || code >= 0xE000) {
      length += 3;
    } else {
      // Surrogate pair (e.g., emoji) - skip next char too
      i++;
      length += 4;
    }
  }
  return length;
}

/**
 * Deep clone an object
 */
function deepClone<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

/**
 * Check if two values are equal (deep comparison)
 */
function deepEqual(a: unknown, b: unknown): boolean {
  return JSON.stringify(a) === JSON.stringify(b);
}

/**
 * Format a value for display
 * @param isSection - Whether this is being displayed as a section header (affects array display)
 */
function formatValue(value: unknown, schema: FieldSchema, isSection: boolean = false): string {
  if (value === undefined || value === null) {
    return "<not set>";
  }
  switch (schema.type) {
    case "boolean":
      return value ? "[x] true" : "[ ] false";
    case "enum":
      return `[▼] ${value}`;
    case "number":
      return String(value);
    case "string":
      if (typeof value === "string" && value.length > 40) {
        return `"${value.substring(0, 37)}..."`;
      }
      return `"${value}"`;
    case "array":
      if (Array.isArray(value)) {
        // When displayed as section, just show count
        if (isSection) {
          return `(${value.length} items)`;
        }
        // For inline display (simple arrays)
        if (value.length === 0) return "[]";
        if (value.length <= 3) return `[${value.map(v => JSON.stringify(v)).join(", ")}]`;
        return `[${value.length} items]`;
      }
      return "[]";
    case "object":
      return "{...}";
    default:
      return String(value);
  }
}

/**
 * Get the default value for a field
 */
function getDefaultValue(schema: FieldSchema): unknown {
  if (schema.defaultValue !== undefined) {
    return schema.defaultValue;
  }
  switch (schema.type) {
    case "boolean": return false;
    case "number": return 0;
    case "string": return "";
    case "array": return [];
    case "object": return {};
    case "enum": return schema.enumOptions?.[0] ?? "";
    default: return null;
  }
}

// =============================================================================
// Config Loading and Saving
// =============================================================================

/**
 * Find the user's config file path
 */
function findConfigPath(): string {
  const cwd = editor.getCwd();

  // Try local config first
  const localConfig = editor.pathJoin(cwd, "config.json");
  if (editor.fileExists(localConfig)) {
    return localConfig;
  }

  // Try ~/.config/fresh/config.json
  const home = editor.getEnv("HOME");
  if (home) {
    const userConfig = editor.pathJoin(home, ".config", "fresh", "config.json");
    if (editor.fileExists(userConfig)) {
      return userConfig;
    }
    // Return this path even if it doesn't exist - we'll create it
    return userConfig;
  }

  // Fallback to local
  return localConfig;
}

/**
 * Load config from file
 */
async function loadConfig(): Promise<Record<string, unknown>> {
  const configPath = findConfigPath();
  state.configPath = configPath;

  try {
    const content = await editor.readFile(configPath);
    return JSON.parse(content);
  } catch (e) {
    // Return empty config if file doesn't exist
    editor.debug(`Config file not found at ${configPath}, using defaults`);
    return {};
  }
}

/**
 * Save config to file
 */
async function saveConfig(): Promise<boolean> {
  try {
    const content = JSON.stringify(state.workingConfig, null, 2);
    await editor.writeFile(state.configPath, content);
    state.originalConfig = deepClone(state.workingConfig);
    state.hasChanges = false;
    return true;
  } catch (e) {
    editor.setStatus(`Failed to save: ${e}`);
    return false;
  }
}

// =============================================================================
// Field Building
// =============================================================================

/**
 * Build the flat list of visible fields based on schema and expanded sections
 * Uses the dynamically loaded schema from state.schema
 */
function buildVisibleFields(): ConfigField[] {
  const fields: ConfigField[] = [];

  /**
   * Recursively add fields from a schema
   */
  function addFieldsFromSchema(
    schema: Record<string, FieldSchema>,
    basePath: string,
    depth: number,
    configData: Record<string, unknown>
  ): void {
    for (const [key, fieldSchema] of Object.entries(schema)) {
      const path = basePath ? `${basePath}.${key}` : key;
      const value = configData[key];
      const defaultValue = getDefaultValue(fieldSchema);
      const isDefault = value === undefined || deepEqual(value, defaultValue);

      // Check if this is an object with nested properties
      if (fieldSchema.type === "object" && fieldSchema.nestedSchema) {
        // Section header
        const expanded = state.expandedSections.has(path);
        fields.push({
          path,
          name: key,
          schema: fieldSchema,
          value,
          isDefault,
          depth,
          isSection: true,
          expanded,
        });

        // Add nested fields if expanded
        if (expanded && fieldSchema.nestedSchema) {
          const nestedData = (value as Record<string, unknown>) || {};
          addFieldsFromSchema(fieldSchema.nestedSchema, path, depth + 1, nestedData);
        }
      }
      // Check if this is a map type (additionalProperties pattern - languages, lsp)
      else if (fieldSchema.type === "object" && fieldSchema.itemSchema && !fieldSchema.nestedSchema) {
        // Map section header
        const expanded = state.expandedSections.has(path);
        fields.push({
          path,
          name: key,
          schema: fieldSchema,
          value,
          isDefault,
          depth,
          isSection: true,
          expanded,
        });

        // Add map entries if expanded
        if (expanded && value && typeof value === "object") {
          const mapData = value as Record<string, unknown>;
          const itemSchema = fieldSchema.itemSchema;

          for (const mapKey of Object.keys(mapData).sort()) {
            const itemPath = `${path}.${mapKey}`;
            const itemValue = mapData[mapKey];
            const itemExpanded = state.expandedSections.has(itemPath);

            // Check if item is an object with nested schema
            if (itemSchema.type === "object" && itemSchema.nestedSchema) {
              fields.push({
                path: itemPath,
                name: mapKey,
                schema: { ...itemSchema, description: `${mapKey} configuration` },
                value: itemValue,
                isDefault: false,
                depth: depth + 1,
                isSection: true,
                expanded: itemExpanded,
              });

              // Add nested fields if expanded
              if (itemExpanded && itemSchema.nestedSchema) {
                const itemData = (itemValue as Record<string, unknown>) || {};
                addFieldsFromSchema(itemSchema.nestedSchema, itemPath, depth + 2, itemData);
              }
            } else {
              // Simple item value
              fields.push({
                path: itemPath,
                name: mapKey,
                schema: itemSchema,
                value: itemValue,
                isDefault: false,
                depth: depth + 1,
                isSection: false,
              });
            }
          }
        }
      }
      // Check if this is an array - treat as expandable section
      else if (fieldSchema.type === "array") {
        const expanded = state.expandedSections.has(path);
        const arrayValue = (value as unknown[]) || [];
        fields.push({
          path,
          name: key,
          schema: fieldSchema,
          value: arrayValue,
          isDefault,
          depth,
          isSection: true,
          expanded,
        });

        // Add array items if expanded
        if (expanded && arrayValue.length > 0) {
          const itemSchema = fieldSchema.itemSchema || { type: "string" as FieldType, description: "Array item" };
          for (let i = 0; i < arrayValue.length; i++) {
            const itemPath = `${path}[${i}]`;
            const itemValue = arrayValue[i];

            // Check if item is a complex object (either by schema or by actual value)
            const isComplexObject = (itemSchema.type === "object" && itemSchema.nestedSchema) ||
              (typeof itemValue === "object" && itemValue !== null && !Array.isArray(itemValue));

            if (isComplexObject) {
              const itemExpanded = state.expandedSections.has(itemPath);

              // Build schema from actual object keys if no nestedSchema defined
              let effectiveSchema = itemSchema;
              if (!itemSchema.nestedSchema && typeof itemValue === "object" && itemValue !== null) {
                effectiveSchema = {
                  ...itemSchema,
                  type: "object" as FieldType,
                  nestedSchema: inferSchemaFromObject(itemValue as Record<string, unknown>),
                };
              }

              fields.push({
                path: itemPath,
                name: `[${i}]`,
                schema: { ...effectiveSchema, description: `Item ${i}` },
                value: itemValue,
                isDefault: false,
                depth: depth + 1,
                isSection: true,
                expanded: itemExpanded,
              });

              // Add nested fields if expanded
              if (itemExpanded && effectiveSchema.nestedSchema) {
                const itemData = (itemValue as Record<string, unknown>) || {};
                addFieldsFromSchema(effectiveSchema.nestedSchema, itemPath, depth + 2, itemData);
              }
            } else {
              // Simple array item
              fields.push({
                path: itemPath,
                name: `[${i}]`,
                schema: itemSchema,
                value: itemValue,
                isDefault: false,
                depth: depth + 1,
                isSection: false,
              });
            }
          }
        }
      }
      // Regular field
      else {
        fields.push({
          path,
          name: key,
          schema: fieldSchema,
          value: value ?? defaultValue,
          isDefault,
          depth,
          isSection: false,
        });
      }
    }
  }

  // Build fields from loaded schema
  addFieldsFromSchema(state.schema, "", 0, state.workingConfig);

  return fields;
}

// =============================================================================
// UI Building
// =============================================================================

/**
 * Build text entries for the virtual buffer display
 */
function buildDisplayEntries(): TextPropertyEntry[] {
  const entries: TextPropertyEntry[] = [];

  // Title
  entries.push({
    text: "━━━ Fresh Configuration Editor ━━━\n",
    properties: { type: "title" },
  });
  entries.push({
    text: `File: ${state.configPath}${state.hasChanges ? " [modified]" : ""}\n`,
    properties: { type: "file-path" },
  });
  entries.push({
    text: "\n",
    properties: { type: "blank" },
  });

  // Fields
  state.visibleFields = buildVisibleFields();

  for (let i = 0; i < state.visibleFields.length; i++) {
    const field = state.visibleFields[i];
    const indent = "  ".repeat(field.depth);

    // Add description before the field if it has one (and it's not just the field name)
    const desc = field.schema.description;
    if (desc && desc !== field.name && !desc.startsWith("Item ")) {
      const descIndent = indent + "  ";
      // Split on newlines and indent each line, prefix with // to indicate comment
      for (const line of desc.split("\n")) {
        entries.push({
          text: `${descIndent}// ${line}\n`,
          properties: { type: "description", path: field.path },
        });
      }
    }

    if (field.isSection) {
      // Section header with expand/collapse indicator
      const icon = field.expanded ? "▼" : "▶";
      // For arrays, show item count; for objects, just show the name
      const suffix = field.schema.type === "array" && Array.isArray(field.value)
        ? ` (${(field.value as unknown[]).length} items)`
        : "";
      entries.push({
        text: `${indent}${icon} ${field.name}${suffix}\n`,
        properties: {
          type: "section",
          path: field.path,
          index: i,
          expanded: field.expanded,
        },
      });
    } else {
      // Regular field
      const valueStr = formatValue(field.value, field.schema);
      const defaultMarker = field.isDefault ? " (default)" : "";

      entries.push({
        text: `${indent}  ${field.name}: ${valueStr}${defaultMarker}\n`,
        properties: {
          type: "field",
          path: field.path,
          index: i,
          fieldType: field.schema.type,
          isDefault: field.isDefault,
        },
      });
    }

    // Add empty line after each field for readability
    entries.push({
      text: "\n",
      properties: { type: "blank" },
    });
  }

  // Footer with help
  entries.push({
    text: "\n",
    properties: { type: "blank" },
  });
  entries.push({
    text: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n",
    properties: { type: "separator" },
  });
  entries.push({
    text: "↑/↓: navigate | RET/SPC: edit | TAB: expand/collapse | d: reset to default\n",
    properties: { type: "footer" },
  });
  entries.push({
    text: "s: save | r: reload | q: quit | ?: help\n",
    properties: { type: "footer" },
  });

  return entries;
}

/**
 * Helper to add a colored overlay
 * API: addOverlay(buffer_id, namespace, start, end, r, g, b, underline, bold, italic)
 */
function addColorOverlay(
  bufferId: number,
  start: number,
  end: number,
  color: [number, number, number],
  bold: boolean = false
): void {
  editor.addOverlay(bufferId, "config", start, end, color[0], color[1], color[2], false, bold, false);
}

/**
 * Apply syntax highlighting to the config editor buffer
 */
function applyHighlighting(): void {
  if (state.bufferId === null) return;

  const bufferId = state.bufferId;
  editor.clearNamespace(bufferId, "config");

  // Rebuild entries to get properties for highlighting
  const entries = buildDisplayEntries();
  let byteOffset = 0;

  for (const entry of entries) {
    const text = entry.text;
    const textLen = getUtf8ByteLength(text);
    const props = entry.properties as Record<string, unknown>;
    const entryType = props.type as string;

    // Apply colors based on entry type
    if (entryType === "title") {
      // Title - gold/yellow, bold
      addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.sectionHeader, true);
    } else if (entryType === "file-path") {
      // File path - gray
      addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.description);
    } else if (entryType === "description") {
      // Description/comment - subdued gray
      addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.description);
    } else if (entryType === "section") {
      // Section header - gold, bold
      addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.sectionHeader, true);
    } else if (entryType === "field") {
      // Field line - highlight name and value differently
      const fieldType = props.fieldType as string;
      const isDefault = props.isDefault as boolean;

      // Find the colon position to split name from value
      const colonPos = text.indexOf(":");
      if (colonPos > 0) {
        const nameEnd = byteOffset + getUtf8ByteLength(text.substring(0, colonPos));
        const valueStart = nameEnd + getUtf8ByteLength(": ");

        // Field name - light blue
        addColorOverlay(bufferId, byteOffset, nameEnd, colors.fieldName);

        // Value - color based on type and default status
        let valueFg = isDefault ? colors.defaultValue : colors.customValue;
        if (fieldType === "boolean") {
          // Check if true or false
          if (text.includes("[x]")) {
            valueFg = colors.boolTrue;
          } else if (text.includes("[ ]")) {
            valueFg = colors.boolFalse;
          }
        } else if (fieldType === "enum") {
          valueFg = colors.enumValue;
        } else if (fieldType === "number") {
          valueFg = colors.numberValue;
        } else if (fieldType === "string") {
          valueFg = isDefault ? colors.defaultValue : colors.stringValue;
        }

        addColorOverlay(bufferId, valueStart, byteOffset + textLen, valueFg);
      }
    } else if (entryType === "separator" || entryType === "footer") {
      // Footer/separator - dim
      addColorOverlay(bufferId, byteOffset, byteOffset + textLen, colors.footer);
    }

    byteOffset += textLen;
  }

  // Update status with current field description
  const field = getFieldAtCursor();
  if (field) {
    editor.setStatus(field.schema.description || field.name);
  }
}

/**
 * Update the display, preserving cursor position by field path
 */
function updateDisplay(): void {
  if (state.bufferId === null) return;

  // Save current field path before rebuilding
  const currentField = getFieldAtCursor();
  const currentPath = currentField?.path;

  const entries = buildDisplayEntries();
  state.cachedContent = entries.map(e => e.text).join("");
  editor.setVirtualBufferContent(state.bufferId, entries);

  // Restore cursor to the same field (by path) by finding it in the entries
  if (currentPath) {
    let byteOffset = 0;
    for (const entry of entries) {
      const props = entry.properties as Record<string, unknown>;
      // Find the entry that is the actual field/section (not description)
      if (props.path === currentPath && (props.type === "section" || props.type === "field")) {
        editor.setBufferCursor(state.bufferId, byteOffset);
        break;
      }
      byteOffset += getUtf8ByteLength(entry.text);
    }
  }

  applyHighlighting();
}

// =============================================================================
// Field Editing
// =============================================================================

/**
 * Get the field at the current cursor position
 */
function getFieldAtCursor(): ConfigField | null {
  if (state.bufferId === null) return null;

  const props = editor.getTextPropertiesAtCursor(state.bufferId);
  if (props.length > 0 && typeof props[0].index === "number") {
    const index = props[0].index as number;
    if (index >= 0 && index < state.visibleFields.length) {
      return state.visibleFields[index];
    }
  }

  return null;
}

/**
 * Edit a boolean field (toggle)
 */
function editBooleanField(field: ConfigField): void {
  const currentValue = field.value as boolean;
  const newValue = !currentValue;

  setNestedValue(state.workingConfig, field.path, newValue);
  state.hasChanges = !deepEqual(state.workingConfig, state.originalConfig);
  updateDisplay();

  editor.setStatus(`${field.name}: ${newValue}`);
}

/**
 * Edit an enum field (show selection prompt)
 */
function editEnumField(field: ConfigField): void {
  const options = field.schema.enumOptions || [];
  const currentValue = field.value as string;

  // Start a prompt for enum selection
  editor.startPrompt(`${field.name}: `, `config-enum-${field.path}`);

  // Build suggestions
  const suggestions: PromptSuggestion[] = options.map(opt => ({
    text: opt,
    value: opt,
    description: opt === currentValue ? "(current)" : undefined,
  }));

  editor.setPromptSuggestions(suggestions);
}

/**
 * Edit a number field (show input prompt)
 */
function editNumberField(field: ConfigField): void {
  const currentValue = field.value as number;
  const min = field.schema.min;
  const max = field.schema.max;

  let label = `${field.name}`;
  if (min !== undefined || max !== undefined) {
    label += ` (${min ?? ""}..${max ?? ""})`;
  }
  label += ": ";

  editor.startPrompt(label, `config-number-${field.path}`);

  // Show current value as initial suggestion
  editor.setPromptSuggestions([{
    text: String(currentValue),
    description: "(current value)",
    value: String(currentValue),
  }]);
}

/**
 * Edit a string field (show input prompt)
 */
function editStringField(field: ConfigField): void {
  const currentValue = (field.value as string) || "";

  editor.startPrompt(`${field.name}: `, `config-string-${field.path}`);

  // Show current value as suggestion
  if (currentValue) {
    editor.setPromptSuggestions([{
      text: currentValue,
      description: "(current value)",
      value: currentValue,
    }]);
  }
}

// =============================================================================
// Prompt Handlers
// =============================================================================

/**
 * Handle enum selection prompt confirmation
 */
globalThis.onConfigEnumPromptConfirmed = function(args: {
  prompt_type: string;
  selected_index: number | null;
  input: string;
}): boolean {
  if (!args.prompt_type.startsWith("config-enum-")) return true;

  const path = args.prompt_type.replace("config-enum-", "");
  const field = state.visibleFields.find(f => f.path === path);

  if (field && args.input) {
    // Validate against options
    const options = field.schema.enumOptions || [];
    if (options.includes(args.input)) {
      setNestedValue(state.workingConfig, path, args.input);
      state.hasChanges = !deepEqual(state.workingConfig, state.originalConfig);
      updateDisplay();
      editor.setStatus(`${field.name}: ${args.input}`);
    } else {
      editor.setStatus(`Invalid option: ${args.input}`);
    }
  }

  return true;
};

/**
 * Handle number input prompt confirmation
 */
globalThis.onConfigNumberPromptConfirmed = function(args: {
  prompt_type: string;
  selected_index: number | null;
  input: string;
}): boolean {
  if (!args.prompt_type.startsWith("config-number-")) return true;

  const path = args.prompt_type.replace("config-number-", "");
  const field = state.visibleFields.find(f => f.path === path);

  if (field && args.input) {
    const num = parseFloat(args.input);
    if (isNaN(num)) {
      editor.setStatus("Invalid number");
      return true;
    }

    // Validate range
    if (field.schema.min !== undefined && num < field.schema.min) {
      editor.setStatus(`Value must be >= ${field.schema.min}`);
      return true;
    }
    if (field.schema.max !== undefined && num > field.schema.max) {
      editor.setStatus(`Value must be <= ${field.schema.max}`);
      return true;
    }

    setNestedValue(state.workingConfig, path, num);
    state.hasChanges = !deepEqual(state.workingConfig, state.originalConfig);
    updateDisplay();
    editor.setStatus(`${field.name}: ${num}`);
  }

  return true;
};

/**
 * Handle string input prompt confirmation
 */
globalThis.onConfigStringPromptConfirmed = function(args: {
  prompt_type: string;
  selected_index: number | null;
  input: string;
}): boolean {
  if (!args.prompt_type.startsWith("config-string-")) return true;

  const path = args.prompt_type.replace("config-string-", "");
  const field = state.visibleFields.find(f => f.path === path);

  if (field) {
    setNestedValue(state.workingConfig, path, args.input);
    state.hasChanges = !deepEqual(state.workingConfig, state.originalConfig);
    updateDisplay();
    editor.setStatus(`${field.name}: "${args.input}"`);
  }

  return true;
};

/**
 * Handle prompt cancellation
 */
globalThis.onConfigPromptCancelled = function(args: { prompt_type: string }): boolean {
  if (!args.prompt_type.startsWith("config-")) return true;
  editor.setStatus("Cancelled");
  return true;
};

// Register prompt handlers
editor.on("prompt_confirmed", "onConfigEnumPromptConfirmed");
editor.on("prompt_confirmed", "onConfigNumberPromptConfirmed");
editor.on("prompt_confirmed", "onConfigStringPromptConfirmed");
editor.on("prompt_cancelled", "onConfigPromptCancelled");

// =============================================================================
// Cursor Movement Handler
// =============================================================================

globalThis.onConfigEditorCursorMoved = function(data: {
  buffer_id: number;
  cursor_id: number;
  old_position: number;
  new_position: number;
}): void {
  if (state.bufferId === null || data.buffer_id !== state.bufferId) return;

  // Re-apply highlighting to update selection
  applyHighlighting();

  // Show field description in status
  const field = getFieldAtCursor();
  if (field) {
    editor.setStatus(field.schema.description);
  }
};

editor.on("cursor_moved", "onConfigEditorCursorMoved");

// Buffer activation/deactivation hooks removed in favor of custom context system
// Commands are now registered at startup with "config-editor" context requirement
// The context is activated/deactivated when opening/closing the config editor

// =============================================================================
// Public Commands
// =============================================================================

/**
 * Open the config editor
 */
globalThis.open_config_editor = async function(): Promise<void> {
  if (state.isOpen) {
    editor.setStatus("Config editor already open");
    return;
  }

  editor.setStatus("Loading configuration...");

  // Save context
  state.sourceSplitId = editor.getActiveSplitId();
  state.sourceBufferId = editor.getActiveBufferId();

  // Load schema from JSON file (auto-generated from Rust config structs)
  state.schema = await getConfigSchema();
  editor.debug(`Loaded schema with ${Object.keys(state.schema).length} top-level fields`);

  // Load config
  state.originalConfig = await loadConfig();
  state.workingConfig = deepClone(state.originalConfig);
  state.hasChanges = false;

  // Build initial entries
  const entries = buildDisplayEntries();
  state.cachedContent = entries.map(e => e.text).join("");

  // Create virtual buffer in the current split as a new tab
  const bufferId = await editor.createVirtualBuffer({
    name: "*Config Editor*",
    mode: "config-editor",
    read_only: true,
    entries: entries,
    show_line_numbers: false,
    show_cursors: true,
    editing_disabled: true,
  });

  if (bufferId !== null) {
    state.isOpen = true;
    state.bufferId = bufferId;
    state.splitId = state.sourceSplitId;  // We're using the same split

    // Activate the config-editor context to enable context-specific commands
    editor.setContext("config-editor", true);

    applyHighlighting();
    editor.setStatus("Config Editor | ↑/↓: navigate | RET: edit | s: save | q: quit");
  } else {
    editor.setStatus("Failed to open config editor");
  }
};

/**
 * Close the config editor
 */
globalThis.config_editor_close = function(): void {
  if (!state.isOpen) return;

  // Check for unsaved changes
  if (state.hasChanges) {
    // For now, just warn - could add a confirmation prompt
    editor.setStatus("Warning: Unsaved changes discarded");
  }

  // Deactivate the config-editor context to hide context-specific commands
  editor.setContext("config-editor", false);

  // Close the config editor buffer (will auto-switch to previous buffer in split)
  if (state.bufferId !== null) {
    editor.closeBuffer(state.bufferId);
  }

  // Reset state
  state.isOpen = false;
  state.bufferId = null;
  state.splitId = null;
  state.originalConfig = {};
  state.workingConfig = {};
  state.hasChanges = false;

  editor.setStatus("Config editor closed");
};

/**
 * Edit the field at cursor
 */
globalThis.config_editor_edit_field = function(): void {
  const field = getFieldAtCursor();
  if (!field) {
    editor.setStatus("No field selected");
    return;
  }

  if (field.isSection) {
    // Toggle section expansion
    config_editor_toggle_section();
    return;
  }

  // Edit based on type
  switch (field.schema.type) {
    case "boolean":
      editBooleanField(field);
      break;
    case "enum":
      editEnumField(field);
      break;
    case "number":
      editNumberField(field);
      break;
    case "string":
      editStringField(field);
      break;
    case "array":
    case "object":
      // Toggle expansion for arrays and nested objects
      config_editor_toggle_section();
      break;
    default:
      editor.setStatus(`Cannot edit ${field.schema.type} fields yet`);
  }
};

/**
 * Toggle boolean or edit other field types (Space key)
 */
globalThis.config_editor_toggle_or_edit = function(): void {
  const field = getFieldAtCursor();
  if (!field) {
    editor.setStatus("No field selected");
    return;
  }

  if (field.isSection) {
    config_editor_toggle_section();
    return;
  }

  if (field.schema.type === "boolean") {
    editBooleanField(field);
  } else {
    config_editor_edit_field();
  }
};

/**
 * Toggle section expansion
 */
globalThis.config_editor_toggle_section = function(): void {
  const field = getFieldAtCursor();
  if (!field || !field.isSection) {
    editor.setStatus("Not a section");
    return;
  }

  if (state.expandedSections.has(field.path)) {
    state.expandedSections.delete(field.path);
  } else {
    state.expandedSections.add(field.path);
  }

  updateDisplay();
};

/**
 * Reset field to default value
 */
globalThis.config_editor_reset_to_default = function(): void {
  const field = getFieldAtCursor();
  if (!field || field.isSection) {
    editor.setStatus("Select a field to reset");
    return;
  }

  if (field.isDefault) {
    editor.setStatus("Already using default value");
    return;
  }

  // Remove the custom value (will fall back to default)
  deleteNestedValue(state.workingConfig, field.path);
  state.hasChanges = !deepEqual(state.workingConfig, state.originalConfig);
  updateDisplay();

  editor.setStatus(`${field.name} reset to default`);
};

/**
 * Save configuration
 */
globalThis.config_editor_save = async function(): Promise<void> {
  if (!state.hasChanges) {
    editor.setStatus("No changes to save");
    return;
  }

  editor.setStatus("Saving...");

  if (await saveConfig()) {
    updateDisplay();
    editor.setStatus(`Configuration saved to ${state.configPath}`);
  }
};

/**
 * Reload configuration from file
 */
globalThis.config_editor_reload = async function(): Promise<void> {
  if (state.hasChanges) {
    editor.setStatus("Discarding local changes...");
  }

  state.originalConfig = await loadConfig();
  state.workingConfig = deepClone(state.originalConfig);
  state.hasChanges = false;
  updateDisplay();

  editor.setStatus("Configuration reloaded from file");
};

/**
 * Show help
 */
globalThis.config_editor_show_help = function(): void {
  editor.setStatus(
    "Keys: ↑/↓ navigate | RET/SPC edit | TAB expand | d default | s save | r reload | q quit"
  );
};

// =============================================================================
// Command Registration
// =============================================================================

// Register the open command permanently (always available, no custom context required)
editor.registerCommand(
  "Edit Configuration",
  "Open the configuration editor",
  "open_config_editor",
  "normal"
);

// Register config editor commands with "config-editor" custom context
// These commands will only be enabled when the config-editor context is active
editor.registerCommand(
  "Config: Close Editor",
  "Close the config editor",
  "config_editor_close",
  "normal,config-editor"  // Requires both normal context AND config-editor custom context
);

editor.registerCommand(
  "Config: Edit Field",
  "Edit the selected field",
  "config_editor_edit_field",
  "normal,config-editor"
);

editor.registerCommand(
  "Config: Toggle/Edit",
  "Toggle boolean or edit field",
  "config_editor_toggle_or_edit",
  "normal,config-editor"
);

editor.registerCommand(
  "Config: Toggle Section",
  "Expand or collapse section",
  "config_editor_toggle_section",
  "normal,config-editor"
);

editor.registerCommand(
  "Config: Reset to Default",
  "Reset field to default value",
  "config_editor_reset_to_default",
  "normal,config-editor"
);

editor.registerCommand(
  "Config: Save",
  "Save configuration",
  "config_editor_save",
  "normal,config-editor"
);

editor.registerCommand(
  "Config: Reload",
  "Reload configuration from file",
  "config_editor_reload",
  "normal,config-editor"
);

editor.registerCommand(
  "Config: Show Help",
  "Show config editor help",
  "config_editor_show_help",
  "normal,config-editor"
);

// =============================================================================
// Plugin Initialization
// =============================================================================

editor.setStatus("Config Editor plugin loaded");
editor.debug("Config Editor plugin initialized - Use 'Edit Configuration' command to open");
