import { depsOfClass, getMetadata, isClass, type Token, type Type } from '@frontmcp/di';
import { isValidMcpUri } from '@frontmcp/utils';

import {
  AppKind,
  FrontMcpLocalAppTokens,
  type AppEntry,
  type AppRecord,
  type AppType,
  type LocalAppMetadata,
  type RemoteAppMetadata,
} from '../common';
import { InvalidEntityError, MissingProvideError } from '../errors';
import { type AppLocalInstance } from './instances';

export function collectAppMetadata(cls: AppType): LocalAppMetadata {
  return Object.entries(FrontMcpLocalAppTokens).reduce((metadata, [key, token]) => {
    return Object.assign(metadata, {
      [key]: getMetadata(token, cls),
    });
  }, {} as LocalAppMetadata);
}

/**
 * Check if an object is a remote app configuration.
 * For npm/esm urlTypes the url is a package specifier (no scheme required).
 * For url/worker urlTypes the url must have a valid URI scheme per RFC 3986.
 */
function isRemoteAppConfig(item: unknown): item is RemoteAppMetadata {
  if (!item || typeof item !== 'object') {
    return false;
  }
  const obj = item as Record<string, unknown>;
  if (typeof obj['urlType'] !== 'string' || typeof obj['url'] !== 'string' || typeof obj['name'] !== 'string') {
    return false;
  }
  // For npm/esm urlTypes, url is a package specifier (no scheme required)
  if (obj['urlType'] === 'npm' || obj['urlType'] === 'esm') {
    return true;
  }
  // For url/worker types, require a valid URI scheme
  if (obj['urlType'] === 'url' || obj['urlType'] === 'worker') {
    return isValidMcpUri(obj['url']);
  }
  // Unknown urlType — reject
  return false;
}

/**
 * Normalize a raw app metadata list into useful maps/sets.
 * - tokens: all provided tokens
 * - defs: AppRecord by token
 * - graph: initialized adjacency map (empty sets)
 */
export function normalizeApp(item: AppType): AppRecord {
  if (isClass(item)) {
    const metadata = collectAppMetadata(item);
    return { kind: AppKind.LOCAL_CLASS, provide: item as Type<AppLocalInstance>, metadata };
  }

  // Check for remote app configuration (has urlType, url, and name)
  if (isRemoteAppConfig(item)) {
    const metadata = item; // Type guard already narrows to RemoteAppMetadata
    const appId = metadata.id ?? metadata.name;
    const provide: Token = Symbol(`remote:${appId}`);

    return {
      kind: AppKind.REMOTE_VALUE,
      provide,
      // Placeholder: Remote instances are created in AppRegistry.initialize
      // The null is replaced with actual AppRemoteInstance during initialization
      useValue: null as unknown as AppEntry,
      metadata,
    };
  }

  if (item && typeof item === 'object') {
    const { provide, useValue, ...metadata } = item as any;
    if (!provide) {
      const name = (item as any)?.name ?? JSON.stringify(item) ?? '[object]';
      throw new MissingProvideError('App', name);
    }

    if (useValue) {
      return {
        kind: AppKind.REMOTE_VALUE,
        provide,
        useValue,
        metadata,
      };
    }
  }
  const name = (item as any)?.name ?? String(item);
  throw new InvalidEntityError('app', name, 'a class or remote app config object');
}

/**
 * For graph/cycle detection. Returns dependency tokens that should be graphed.
 * - REMOTE_VALUE: no deps
 * - LOCAL_CLASS: deps come from the class constructor
 */
export function appDiscoveryDeps(rec: AppRecord): Token[] {
  switch (rec.kind) {
    case AppKind.REMOTE_VALUE:
      return [];
    case AppKind.LOCAL_CLASS:
      return depsOfClass(rec.provide, 'discovery');
  }
}
