/**
 * vWhale: schema-based tool input repair. When the model emits tool call args
 * that don't match the expected types (common with weaker models), apply
 * heuristic repairs before falling through to the Zod parse.
 *
 * Inspired by Whale's RepairToolInputForSpec:
 *   - Coerce string "true"/"false" → boolean
 *   - Unwrap markdown autolinks in string fields: <file:///path> → /path
 *   - Wrap bare values in arrays when schema expects array
 *   - Convert "42.0" decimal string → "42" for integer fields
 *   - Recurse into objects to repair nested properties
 */

export interface ToolInputRepair {
  field: string;
  kind: string;
  detail: string;
}

const MARKDOWN_AUTOLINK_RE = /^<(?:file|path):\/\/(.+?)>$/;

/**
 * Attempt to repair tool call args against the tool's JSON Schema.
 * Returns the (possibly modified) args plus a list of repairs applied.
 */
export function repairToolInput(
  schema: Record<string, unknown> | undefined,
  args: Record<string, unknown>,
): { repaired: Record<string, unknown>; repairs: ToolInputRepair[] } {
  const repairs: ToolInputRepair[] = [];
  if (!schema || typeof schema !== 'object') {
    return { repaired: args, repairs };
  }

  const properties = (schema as Record<string, unknown>).properties as
    Record<string, unknown> | undefined;
  if (!properties) {
    return { repaired: args, repairs };
  }

  const required = new Set<string>(
    Array.isArray((schema as Record<string, unknown>).required)
      ? (schema as Record<string, unknown>).required as string[]
      : [],
  );

  const repaired: Record<string, unknown> = {};
  for (const [key, value] of Object.entries(args)) {
    const propSchema = properties[key] as Record<string, unknown> | undefined;
    if (propSchema && value !== null && value !== undefined) {
      repaired[key] = repairValue(key, propSchema, value, repairs, required.has(key));
    } else {
      repaired[key] = value;
    }
  }

  // Drop keys not in the schema (only for required fields that are missing)
  // to avoid polluting the model with hallucinated params.
  for (const key of Object.keys(repaired)) {
    if (!(key in properties)) {
      repairs.push({ field: key, kind: 'removed_unknown', detail: `Removed unknown parameter '${key}'` });
      delete repaired[key];
    }
  }

  return { repaired, repairs };
}

function repairValue(
  field: string,
  schema: Record<string, unknown>,
  value: unknown,
  repairs: ToolInputRepair[],
  required: boolean,
): unknown {
  const schemaType = schema.type;
  const isArray = schemaType === 'array' || Array.isArray(schemaType)
    ? schemaType === 'array' || (Array.isArray(schemaType) && schemaType.includes('array'))
    : false;
  const isObject = schemaType === 'object';
  const isBoolean = schemaType === 'boolean';
  const isInteger = schemaType === 'integer' || schemaType === 'number';
  const isString = schemaType === 'string';

  if (isArray) {
    if (!Array.isArray(value)) {
      if (typeof value === 'string') {
        // Try parsing as JSON array first
        try {
          const parsed = JSON.parse(value);
          if (Array.isArray(parsed)) {
            repairs.push({ field, kind: 'parsed_json_array', detail: `Parsed string as JSON array for '${field}'` });
            return parsed;
          }
        } catch { /* not JSON */ }
      }
      if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) {
        if (required) {
          repairs.push({ field, kind: 'empty_object_to_array', detail: `Converted empty object to empty array for '${field}'` });
          return [];
        }
        repairs.push({ field, kind: 'empty_object_to_undefined', detail: `Removed empty object for optional array '${field}'` });
        return undefined;
      }
      repairs.push({ field, kind: 'wrapped_in_array', detail: `Wrapped bare value in array for '${field}'` });
      return [value];
    }
    // Recurse into array items
    const itemsSchema = schema.items as Record<string, unknown> | undefined;
    if (itemsSchema) {
      return value.map((item, i) => repairValue(`${field}[${i}]`, itemsSchema, item, repairs, required));
    }
    return value;
  }

  if (isObject && typeof value === 'object' && value !== null && !Array.isArray(value)) {
    const props = (schema.properties as Record<string, unknown>) ?? {};
    const repaired: Record<string, unknown> = {};
    for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
      const propSchema = props[k] as Record<string, unknown> | undefined;
      if (propSchema) {
        repaired[k] = repairValue(`${field}.${k}`, propSchema, v, repairs, required);
      } else {
        repaired[k] = v;
      }
    }
    return repaired;
  }

  if (isString && typeof value === 'string') {
    const match = value.match(MARKDOWN_AUTOLINK_RE);
    if (match) {
      repairs.push({ field, kind: 'unwrapped_markdown_link', detail: `Unwrapped markdown autolink for '${field}': ${value}` });
      return match[1];
    }
    return value;
  }

  if (isBoolean && typeof value === 'string') {
    const lower = value.toLowerCase();
    if (lower === 'true') {
      repairs.push({ field, kind: 'coerced_to_boolean', detail: `Coerced string '${value}' → true for '${field}'` });
      return true;
    }
    if (lower === 'false') {
      repairs.push({ field, kind: 'coerced_to_boolean', detail: `Coerced string '${value}' → false for '${field}'` });
      return false;
    }
    return value;
  }

  if (isInteger && typeof value === 'string') {
    const num = Number(value);
    if (!Number.isNaN(num)) {
      repairs.push({ field, kind: 'coerced_to_number', detail: `Coerced string '${value}' → ${num} for '${field}'` });
      return num;
    }
    return value;
  }

  if (isInteger && typeof value === 'boolean') {
    repairs.push({ field, kind: 'coerced_boolean_to_integer', detail: `Coerced boolean ${value} → ${value ? 1 : 0} for '${field}'` });
    return value ? 1 : 0;
  }

  if (value === '' && !required) {
    repairs.push({ field, kind: 'empty_string_to_undefined', detail: `Converted empty string for optional '${field}'` });
    return undefined;
  }

  return value;
}
