/**
 * @fileoverview Per-canvas handle returned by {@link DataCanvas.acquire}. Holds
 * `(canvasId, tenantId)` and routes each operation through the registry's
 * TTL-touch + tenant validation gate before delegating to the provider.
 * @module src/services/canvas/core/CanvasInstance
 */

import type { RequestContextLike } from '@/utils/internal/requestContext.js';
import type {
  DescribeOptions,
  ExportOptions,
  ExportResult,
  ExportTarget,
  ImportFromOptions,
  QueryOptions,
  QueryResult,
  RegisterRows,
  RegisterTableOptions,
  RegisterTableResult,
  RegisterViewOptions,
  RegisterViewResult,
  TableInfo,
} from '../types.js';
import type { CanvasRegistry } from './CanvasRegistry.js';
import type { IDataCanvasProvider } from './IDataCanvasProvider.js';

/** Handle bound to a single canvas. Returned from {@link DataCanvas.acquire}. */
export class CanvasInstance {
  /** True when {@link DataCanvas.acquire} created this canvas during the current call. */
  readonly isNew: boolean;
  /** ISO 8601 expiry after the most recent operation extended the sliding TTL. */
  expiresAt: string;

  constructor(
    /** Opaque canvas token. Surface this to callers; share it across agents. */
    readonly canvasId: string,
    /** Tenant the canvas is bound to. Resolved by the registry from the request context. */
    readonly tenantId: string,
    isNew: boolean,
    expiresAt: string,
    private readonly registry: CanvasRegistry,
    private readonly provider: IDataCanvasProvider,
    private readonly context: RequestContextLike,
  ) {
    this.isNew = isNew;
    this.expiresAt = expiresAt;
  }

  /** Register a table on the canvas. */
  async registerTable(
    name: string,
    rows: RegisterRows,
    options?: RegisterTableOptions,
  ): Promise<RegisterTableResult> {
    this.expiresAt = this.registry.touchWithTable(this.canvasId, this.tenantId, name);
    const result = await this.provider.registerTable(
      this.canvasId,
      name,
      rows,
      this.context,
      options,
    );
    if (options?.ttlMs !== undefined) {
      this.registry.registerTableTtl(this.canvasId, this.tenantId, name, options.ttlMs);
    } else {
      // The provider replaces an existing table — the new registration's options
      // fully describe its lifecycle, so a stale per-table TTL must not survive.
      this.registry.dropTableBookkeeping(this.canvasId, this.tenantId, name);
    }
    return result;
  }

  /**
   * Register a SQL view on the canvas. `selectSql` runs through the same
   * read-only gate `query()` uses; queries against the view inherit normal
   * gate enforcement at execution time.
   */
  async registerView(
    name: string,
    selectSql: string,
    options?: RegisterViewOptions,
  ): Promise<RegisterViewResult> {
    this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
    return await this.provider.registerView(this.canvasId, name, selectSql, this.context, options);
  }

  /**
   * Copy a table from another canvas the caller controls. The source canvas
   * must belong to the caller's tenant — the registry validates that before
   * the provider sees either id. Defaults `asName` to the source name.
   */
  async importFrom(
    sourceCanvasId: string,
    sourceTableName: string,
    options?: ImportFromOptions,
  ): Promise<RegisterTableResult> {
    this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
    this.registry.touchWithTable(sourceCanvasId, this.tenantId, sourceTableName);
    const asName = options?.asName ?? sourceTableName;
    const result = await this.provider.importFrom(
      this.canvasId,
      sourceCanvasId,
      sourceTableName,
      asName,
      this.context,
      options,
    );
    // The provider replaces an existing destination table; the imported copy
    // carries no per-table TTL, so clear any stale entry under that name.
    this.registry.dropTableBookkeeping(this.canvasId, this.tenantId, asName);
    return result;
  }

  /** Run a SQL query against the canvas. */
  async query(sql: string, options?: QueryOptions): Promise<QueryResult> {
    this.expiresAt = this.registry.touchWithSqlTables(
      this.canvasId,
      this.tenantId,
      options?.registerAs,
      sql,
    );
    const result = await this.provider.query(this.canvasId, sql, this.context, options);
    if (options?.registerAs !== undefined && options.ttlMs !== undefined) {
      this.registry.registerTableTtl(
        this.canvasId,
        this.tenantId,
        options.registerAs,
        options.ttlMs,
      );
    }
    return result;
  }

  /** Export a canvas table to a path or stream target. */
  async export(
    tableName: string,
    target: ExportTarget,
    options?: ExportOptions,
  ): Promise<ExportResult> {
    this.expiresAt = this.registry.touchWithTable(this.canvasId, this.tenantId, tableName);
    return await this.provider.export(this.canvasId, tableName, target, this.context, options);
  }

  /** Describe one or all canvas tables. Per-table `expiresAt` is injected for tables with a TTL. */
  async describe(options?: DescribeOptions): Promise<TableInfo[]> {
    this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
    const result = await this.provider.describe(this.canvasId, this.context, options);
    return this.registry.annotateDescribeResult(this.canvasId, this.tenantId, result);
  }

  /** Drop a single canvas table. Returns `true` when found and removed. */
  async drop(name: string): Promise<boolean> {
    this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
    const result = await this.provider.drop(this.canvasId, name, this.context);
    if (result) {
      this.registry.dropTableBookkeeping(this.canvasId, this.tenantId, name);
    }
    return result;
  }

  /** Drop every table on the canvas. Returns the number dropped. */
  async clear(): Promise<number> {
    this.expiresAt = this.registry.touchOrThrow(this.canvasId, this.tenantId);
    const count = await this.provider.clear(this.canvasId, this.context);
    this.registry.clearTableBookkeeping(this.canvasId, this.tenantId);
    return count;
  }
}
