// src/services/IntentMatcher.ts
// Layer 2 Intent Matching: action + category + dataType scoring.
// Builds on Layer 1 (exact interface name) with three-part filter matching.

export interface IntentFilter {
  actions: string[];
  categories: string[];
  dataTypes: string[];
  priority?: number;
}

export interface IntentQuery {
  action?: string;
  category?: string;
  dataType?: string;
}

export interface IntentMatch {
  mount: string;
  componentClass: string;
  score: number;
  matchReasons: string[];
  intentFilter: IntentFilter;
}

export interface RegisteredHandler {
  mount: string;
  componentClass: string;
  intentFilters: IntentFilter[];
}

type IntentFilterInput = Partial<IntentFilter> & {
  action?: string;
  category?: string;
  dataType?: string;
};

/**
 * IntentMatcher — Three-part matching for capability-based resolution.
 *
 * Matches actors by (action, category, dataType) against their declared
 * `static intentFilters`. Scoring: exact match = 1.0, wildcard = 0.3, miss = 0.
 * Combined score = avg(action, category, dataType) * priority multiplier.
 */
export class IntentMatcher {
  private _handlers: RegisteredHandler[] = [];

  register(handler: RegisteredHandler): void {
    // Prevent duplicates
    const idx = this._handlers.findIndex(h => h.mount === handler.mount);
    if (idx >= 0) {
      this._handlers[idx] = handler;
    } else {
      this._handlers.push(handler);
    }
  }

  unregister(mount: string): boolean {
    const idx = this._handlers.findIndex(h => h.mount === mount);
    if (idx >= 0) {
      this._handlers.splice(idx, 1);
      return true;
    }
    return false;
  }

  resolve(query: IntentQuery): IntentMatch[] {
    const results: IntentMatch[] = [];

    for (const handler of this._handlers) {
      for (const filter of handler.intentFilters) {
        const { score, reasons } = this._scoreMatch(query, filter);
        if (score > 0) {
          const priority = filter.priority ?? 1;
          results.push({
            mount: handler.mount,
            componentClass: handler.componentClass,
            score: score * priority,
            matchReasons: reasons,
            intentFilter: filter,
          });
        }
      }
    }

    // Sort descending by score
    results.sort((a, b) => b.score - a.score);
    return results;
  }

  /**
   * Convenience: resolve and return the best match, or undefined.
   */
  resolveBest(query: IntentQuery): IntentMatch | undefined {
    const matches = this.resolve(query);
    return matches[0];
  }

  /**
   * Get all registered handlers.
   */
  listHandlers(): RegisteredHandler[] {
    return [...this._handlers];
  }

  /**
   * Populate from introspect results (e.g., from $catalog output).
   * Handles both array format {actions:[...]} and single format {action:'...'}.
   */
  registerFromCatalog(entries: Array<{
    mount: string;
    componentClass: string;
    intentFilters?: unknown[];
  }>): number {
    let count = 0;
    for (const entry of entries) {
      if (entry.intentFilters && entry.intentFilters.length > 0) {
        const normalized = entry.intentFilters.map(f => IntentMatcher.normalizeFilter(f));
        this.register({
          mount: entry.mount,
          componentClass: entry.componentClass,
          intentFilters: normalized,
        });
        count++;
      }
    }
    return count;
  }

  /**
   * Normalize an intent filter from either format:
   *   { action: 'query', category: 'data', dataType: 'mx:table-data' }
   *   { actions: ['query'], categories: ['data'], dataTypes: ['mx:table-data'] }
   */
  static normalizeFilter(raw: unknown): IntentFilter {
    const input = (raw && typeof raw === 'object' ? raw : {}) as IntentFilterInput;
    const toArray = (val: string | string[] | undefined): string[] => {
      if (!val) return ['*'];
      return Array.isArray(val) ? val : [val];
    };
    return {
      actions: toArray(input.actions ?? input.action),
      categories: toArray(input.categories ?? input.category),
      dataTypes: toArray(input.dataTypes ?? input.dataType),
      priority: input.priority,
    };
  }

  private _scoreMatch(
    query: IntentQuery,
    filter: IntentFilter
  ): { score: number; reasons: string[] } {
    const reasons: string[] = [];
    let total = 0;
    let parts = 0;

    // Action matching
    if (query.action) {
      parts++;
      const actionScore = this._matchField(query.action, filter.actions);
      total += actionScore;
      if (actionScore > 0) {
        reasons.push(actionScore >= 1 ? `action:exact(${query.action})` : `action:wildcard`);
      }
    }

    // Category matching
    if (query.category) {
      parts++;
      const catScore = this._matchField(query.category, filter.categories);
      total += catScore;
      if (catScore > 0) {
        reasons.push(catScore >= 1 ? `category:exact(${query.category})` : `category:wildcard`);
      }
    }

    // DataType matching
    if (query.dataType) {
      parts++;
      const dtScore = this._matchDataType(query.dataType, filter.dataTypes);
      total += dtScore;
      if (dtScore > 0) {
        reasons.push(dtScore >= 1 ? `dataType:exact(${query.dataType})` : `dataType:wildcard`);
      }
    }

    if (parts === 0) return { score: 0, reasons: [] };

    const avgScore = total / parts;
    return { score: avgScore, reasons };
  }

  /**
   * Match a query value against a list of filter values.
   * Exact match = 1.0, wildcard ('*') = 0.3, no match = 0.
   */
  private _matchField(queryValue: string, filterValues: string[]): number {
    const lower = queryValue.toLowerCase();
    for (const val of filterValues) {
      if (val === '*') return 0.3;
      if (val.toLowerCase() === lower) return 1.0;
    }
    return 0;
  }

  /**
   * Match data types with MIME-style wildcard support.
   * 'mx:table-data' matches 'mx:table-data' exactly (1.0)
   * 'mx:*' matches 'mx:table-data' partially (0.5)
   * '*' matches anything (0.3)
   */
  private _matchDataType(queryType: string, filterTypes: string[]): number {
    const lower = queryType.toLowerCase();
    let best = 0;

    for (const ft of filterTypes) {
      if (ft === '*') {
        best = Math.max(best, 0.3);
        continue;
      }
      const ftLower = ft.toLowerCase();
      if (ftLower === lower) return 1.0; // Exact match

      // MIME-style wildcard: 'mx:*' matches 'mx:anything'
      if (ftLower.endsWith(':*') || ftLower.endsWith('/*')) {
        const prefix = ftLower.slice(0, -1);
        if (lower.startsWith(prefix)) {
          best = Math.max(best, 0.5);
        }
      }
    }

    return best;
  }
}
