/**
 * Abstract base class for visual-grounding model adapters.
 *
 * "Grounding" here means: given a screenshot + an intent description,
 * return either pixel coordinates or a sequence of tool calls that
 * accomplish the intent. Concrete subclasses (Kimi, Claude, OpenAI
 * Computer-Use-Preview, UI-TARS) implement the actual provider call.
 *
 * Why an abstract base instead of duck-typed objects: this is the
 * single seam where adapters get swapped at runtime via the registry.
 * A typed surface lets the form-generator UI introspect knobs, the
 * router pick adapters by capability, and tests use a deterministic
 * MockAdapter with zero special-casing.
 *
 * @typedef {import('./types.js').GroundOptions}     GroundOptions
 * @typedef {import('./types.js').GroundResult}      GroundResult
 * @typedef {import('./types.js').KnobDef}           KnobDef
 * @typedef {import('./types.js').Capabilities}      Capabilities
 * @typedef {import('./types.js').ValidationResult}  ValidationResult
 */

import { ErrorCode } from './constants.js';

export class GroundingModel {
  /**
   * Human-readable name shown in admin UI dropdowns.
   * Subclasses override.
   */
  static displayName = 'GroundingModel (abstract)';

  /**
   * Stable registry key. Same value as in `ModelId`. Subclasses override.
   */
  static modelId = 'abstract';

  // ─── Public API every adapter must implement ──────────────────────

  /**
   * Capability descriptor consumed by:
   *  - the router (picks best adapter for a given op)
   *  - admin UI (renders icons / disables incompatible features)
   *  - the agent's tool description (model self-knows what it can do)
   *
   * @returns {Capabilities}
   */
  capabilities() {
    return {
      pixelGrounding:   false,
      toolCalling:      false,
      multiTurnLoop:    false,
      visionInput:      false,
      maxImagePixels:   0,
      typicalLatencyMs: 0,
      typicalCostPerCallUSD: 0,
    };
  }

  /**
   * Knobs the operator may tune from the admin UI. Drives the
   * dynamic-form generator: each entry becomes a labelled field with
   * type-appropriate widget + inline doc.
   *
   * @returns {KnobDef[]}
   */
  supportedKnobs() {
    return [];
  }

  /**
   * Validate caller-supplied knob values against the schema above.
   * Default impl: any subset of supportedKnobs() is allowed; type
   * coercion is the caller's job. Subclasses override for tighter
   * constraints (e.g. Kimi-specific `reasoning_effort` enum).
   *
   * @param {Object} knobs
   * @returns {ValidationResult}
   */
  validateKnobs(knobs) {
    const errors = {};
    const supported = new Set(this.supportedKnobs().map(k => k.id));
    for (const id of Object.keys(knobs || {})) {
      if (!supported.has(id)) {
        errors[id] = `unknown knob "${id}" (supported: ${[...supported].join(', ')})`;
      }
    }
    return { valid: Object.keys(errors).length === 0, errors };
  }

  /**
   * The main entry point. Subclasses MUST implement.
   *
   * @param {GroundOptions} opts
   * @returns {Promise<GroundResult>}
   */
  async ground(_opts) {
    throw makeError(ErrorCode.INVALID_REQUEST,
      `${this.constructor.name}.ground() not implemented`);
  }
}

/**
 * Construct a typed error the adapters and callers can branch on
 * without parsing message strings. All adapter rejections should
 * route through this so we get a uniform error surface.
 *
 * @param {keyof typeof ErrorCode} code
 * @param {string} message
 * @param {object} [extra]   - additional context attached to .details
 * @returns {Error & { code: string, details?: object }}
 */
export function makeError(code, message, extra = undefined) {
  const err = new Error(message);
  err.code = code;
  if (extra !== undefined) err.details = extra;
  return err;
}
