# Proposal: Extensions Manifest System & Modular MetadataProvider Architecture

**Versi**: 1.0.0  
**Tanggal**: 2024  
**Status**: Draft untuk Review

---

## Executive Summary

Proposal ini mengusulkan implementasi **Extensions Manifest System** dan refactoring arsitektur **MetadataProvider** dari sistem monolitik menjadi arsitektur modular. Sistem baru akan memungkinkan satu extension untuk menyediakan multiple provider types (movie, series, person, credits) melalui deklarasi manifest yang terstruktur.

### Perubahan Utama

1. **Extensions Manifest System**: Deklarasi provider types menggunakan file manifest
2. **Modular Provider Architecture**: Pemisahan provider berdasarkan content type
3. **Provider Registry**: Sistem registrasi dan discovery untuk providers
4. **Special Credits Handling**: Credits provider terintegrasi dengan movie dan series types

---

## 1. Current State Analysis

### 1.1 Masalah dengan Arsitektur Saat Ini

#### **Monolithic Provider Class**
```typescript
// Current: Satu class menangani semua tipe konten
export abstract class MetadataProvider extends Provider {
  abstract search(query): Promise<MetadataSearchResult>;
  abstract resolveMovie(info): Promise<ResolvedMovieMetadataResult>;
  abstract resolveSeries(info): Promise<ResolvedSeriesMetadataResult>;
  abstract resolveTitleCredits(info): Promise<ResolvedTitleCreditsResult>;
  abstract resolvePeople(info): Promise<ResolvedPersonResult>;
}
```

**Masalah**:
- Single class menangani semua operasi metadata (movie, series, person, credits)
- Tidak dapat mengaktifkan/menonaktifkan tipe konten secara independen
- Testing individual components menjadi sulit
- Perubahan pada satu tipe dapat mempengaruhi tipe lainnya
- Kode menjadi besar dan sulit di-maintain

#### **Extension Loading Limitation**
```typescript
// Current: Extension loader mengharapkan single class
export default class TMDBMetadataProviderExtension extends MetadataProvider {
  // Harus implement semua methods, walaupun hanya butuh beberapa
}
```

**Masalah**:
- Extension loader hanya mendukung satu provider class per extension
- Tidak flexible untuk extensions yang ingin menyediakan subset dari capabilities
- Semua methods harus diimplementasi bahkan jika tidak digunakan

#### **Tight Coupling**
- Semua operasi metadata digabung dalam satu abstraksi
- Sulit untuk menambah tipe konten baru
- Dependencies antar tipe konten tidak eksplisit

### 1.2 Current Extension Structure

```
extensions/tmdb-metadata/
├── index.ts                    # Single monolithic class
├── package.json
├── tsconfig.json
└── README.md
```

Current implementation memiliki semua logika dalam satu file dengan 500+ baris kode.

---

## 2. Proposed Solution

### 2.1 Extensions Manifest System

#### **2.1.1 Manifest Interface**

```typescript
// apps/server/src/providers/manifest.ts

/**
 * Definisi untuk satu provider dalam extension
 */
export interface ProviderManifest {
  /** Tipe provider: movie, series, person, atau credits */
  type: 'movie' | 'series' | 'person' | 'credits';
  
  /** Class constructor untuk provider */
  class: new () => any;
  
  /** Nama unik provider */
  name: string;
  
  /** Deskripsi fungsi provider (opsional) */
  description?: string;
  
  /** Versi provider (opsional) */
  version?: string;
  
  /** Apakah provider aktif? Default: true */
  enabled?: boolean;
  
  /** Konfigurasi tambahan provider (opsional) */
  config?: Record<string, any>;
}

/**
 * Manifest utama untuk extension
 */
export interface ExtensionManifest {
  /** Nama extension */
  name: string;
  
  /** Versi extension */
  version: string;
  
  /** Deskripsi extension (opsional) */
  description?: string;
  
  /** Array of providers yang disediakan extension */
  providers: ProviderManifest[];
  
  /** Metadata tambahan extension (opsional) */
  metadata?: {
    author?: string;
    homepage?: string;
    repository?: string;
    documentation?: string;
  };
}
```

#### **2.1.2 Example: TMDB Extension Manifest**

```typescript
// extensions/tmdb-metadata/manifest.ts
import { TMDBMovieProvider } from './providers/movie';
import { TMDBSeriesProvider } from './providers/series';
import { TMDBPersonProvider } from './providers/person';
import type { ExtensionManifest } from '@nontara/server';

export const tmdbManifest: ExtensionManifest = {
  name: "TMDBMetadataProvider",
  version: "1.0.0",
  description: "The Movie Database metadata provider for movies, series, and people",
  
  providers: [
    {
      type: "movie",
      class: TMDBMovieProvider,
      name: "TMDBMovieProvider",
      description: "Provides movie metadata and credits from TMDB API",
      version: "1.0.0",
      enabled: true,
      config: {
        includeCredits: true,  // Movie provider handles credits
        cacheEnabled: true,
        rateLimitPerSecond: 40
      }
    },
    {
      type: "series",
      class: TMDBSeriesProvider,
      name: "TMDBSeriesProvider",
      description: "Provides TV series metadata and credits from TMDB API",
      version: "1.0.0",
      enabled: true,
      config: {
        includeCredits: true,  // Series provider handles credits
        cacheEnabled: true,
        rateLimitPerSecond: 40
      }
    },
    {
      type: "person",
      class: TMDBPersonProvider,
      name: "TMDBPersonProvider",
      description: "Provides person/actor metadata from TMDB API",
      version: "1.0.0",
      enabled: true
    }
    // Note: Tidak ada credits provider terpisah
    // Credits handling terintegrasi dalam movie dan series providers
  ],
  
  metadata: {
    author: "Nontara Team",
    homepage: "https://nontara.dev",
    repository: "https://github.com/nontara/extensions",
    documentation: "https://docs.nontara.dev/extensions/tmdb"
  }
};
```

**Key Points**:
- Satu extension dapat menyediakan multiple providers (movie, series, person)
- Credits tidak memiliki provider terpisah - terintegrasi dengan movie dan series
- Setiap provider dapat memiliki konfigurasi sendiri
- Provider dapat di-enable/disable secara individual

### 2.2 Modular Provider Architecture

#### **2.2.1 Base Provider Interfaces**

```typescript
// apps/server/src/providers/interfaces/movie.ts
import type { MovieInfo, MovieSearchQuery, MovieSearchResult, ResolvedMovieMetadataResult, ResolvedTitleCreditsResult } from '@/models';

export interface BaseMovieProvider {
  /**
   * Search for movies
   */
  search(query: MovieSearchQuery): Promise<MovieSearchResult>;
  
  /**
   * Resolve detailed movie metadata
   */
  resolve(info: MovieInfo): Promise<ResolvedMovieMetadataResult>;
  
  /**
   * Resolve credits for movie (cast & crew)
   * Optional: Beberapa providers mungkin tidak support credits
   */
  resolveCredits?(info: { movieId: string | number }): Promise<ResolvedTitleCreditsResult>;
}
```

```typescript
// apps/server/src/providers/interfaces/series.ts
import type { SeriesInfo, SeriesSearchQuery, SeriesSearchResult, ResolvedSeriesMetadataResult, ResolvedTitleCreditsResult } from '@/models';

export interface BaseSeriesProvider {
  /**
   * Search for TV series
   */
  search(query: SeriesSearchQuery): Promise<SeriesSearchResult>;
  
  /**
   * Resolve detailed series metadata
   */
  resolve(info: SeriesInfo): Promise<ResolvedSeriesMetadataResult>;
  
  /**
   * Resolve credits for series (cast & crew)
   * Optional: Beberapa providers mungkin tidak support credits
   */
  resolveCredits?(info: { seriesId: string | number }): Promise<ResolvedTitleCreditsResult>;
}
```

```typescript
// apps/server/src/providers/interfaces/person.ts
import type { PersonInfo, PersonSearchQuery, PersonSearchResult, ResolvedPersonResult } from '@/models';

export interface BasePersonProvider {
  /**
   * Search for people/actors
   */
  search(query: PersonSearchQuery): Promise<PersonSearchResult>;
  
  /**
   * Resolve detailed person metadata
   */
  resolve(info: PersonInfo): Promise<ResolvedPersonResult>;
}
```

**Important Note on Credits**:
Credits tidak memiliki interface terpisah karena credits handling terintegrasi dengan movie dan series providers melalui optional method `resolveCredits()`.

#### **2.2.2 Provider Registry System**

```typescript
// apps/server/src/providers/registry.ts
import type { BaseMovieProvider } from './interfaces/movie';
import type { BaseSeriesProvider } from './interfaces/series';
import type { BasePersonProvider } from './interfaces/person';

export class MetadataProviderRegistry {
  private movieProviders = new Map<string, BaseMovieProvider>();
  private seriesProviders = new Map<string, BaseSeriesProvider>();
  private personProviders = new Map<string, BasePersonProvider>();
  
  // Extension tracking
  private extensionProviders = new Map<string, Map<string, any>>();

  /**
   * Register movie provider
   */
  registerMovieProvider(name: string, provider: BaseMovieProvider): void {
    this.movieProviders.set(name, provider);
  }

  /**
   * Register series provider
   */
  registerSeriesProvider(name: string, provider: BaseSeriesProvider): void {
    this.seriesProviders.set(name, provider);
  }

  /**
   * Register person provider
   */
  registerPersonProvider(name: string, provider: BasePersonProvider): void {
    this.personProviders.set(name, provider);
  }

  /**
   * Get all registered movie providers
   */
  getMovieProviders(): BaseMovieProvider[] {
    return Array.from(this.movieProviders.values());
  }

  /**
   * Get all registered series providers
   */
  getSeriesProviders(): BaseSeriesProvider[] {
    return Array.from(this.seriesProviders.values());
  }

  /**
   * Get all registered person providers
   */
  getPersonProviders(): BasePersonProvider[] {
    return Array.from(this.personProviders.values());
  }

  /**
   * Get specific movie provider by name
   */
  getMovieProvider(name: string): BaseMovieProvider | undefined {
    return this.movieProviders.get(name);
  }

  /**
   * Get specific series provider by name
   */
  getSeriesProvider(name: string): BaseSeriesProvider | undefined {
    return this.seriesProviders.get(name);
  }

  /**
   * Get specific person provider by name
   */
  getPersonProvider(name: string): BasePersonProvider | undefined {
    return this.personProviders.get(name);
  }

  /**
   * Check if extension supports specific provider type
   */
  extensionSupports(extensionName: string, type: string): boolean {
    const providers = this.extensionProviders.get(extensionName);
    return providers?.has(type) ?? false;
  }

  /**
   * Get all providers from specific extension
   */
  getProvidersByExtension(extensionName: string): Map<string, any> | undefined {
    return this.extensionProviders.get(extensionName);
  }

  /**
   * Register providers from extension manifest
   */
  registerExtension(extensionName: string, providers: Map<string, any>): void {
    this.extensionProviders.set(extensionName, providers);
  }

  /**
   * Unregister all providers from extension
   */
  unregisterExtension(extensionName: string): void {
    const providers = this.extensionProviders.get(extensionName);
    if (providers) {
      // Remove from type-specific registries
      for (const [type, provider] of providers) {
        const key = `${extensionName}.${type}`;
        this.movieProviders.delete(key);
        this.seriesProviders.delete(key);
        this.personProviders.delete(key);
      }
      this.extensionProviders.delete(extensionName);
    }
  }

  /**
   * Clear all registered providers
   */
  clear(): void {
    this.movieProviders.clear();
    this.seriesProviders.clear();
    this.personProviders.clear();
    this.extensionProviders.clear();
  }
}
```

### 2.3 Extension Implementation Structure

#### **2.3.1 New Extension Structure**

```
extensions/tmdb-metadata/
├── manifest.ts                 # Extension manifest definition
├── index.ts                    # Main extension class
├── providers/
│   ├── movie.ts               # TMDBMovieProvider implementation
│   ├── series.ts              # TMDBSeriesProvider implementation
│   └── person.ts              # TMDBPersonProvider implementation
├── shared/
│   ├── client.ts              # Shared TMDB client
│   └── types.ts               # Shared types and interfaces
├── package.json
├── tsconfig.json
└── README.md
```

#### **2.3.2 Main Extension Class**

```typescript
// extensions/tmdb-metadata/index.ts
import type { ConsolaInstance } from 'consola';
import { createLogger } from '@nontara/server';
import { tmdbManifest } from './manifest';
import { TMDBMovieProvider } from './providers/movie';
import { TMDBSeriesProvider } from './providers/series';
import { TMDBPersonProvider } from './providers/person';
import { createTMDBClient } from './shared/client';
import type { TMDB } from 'tmdb-ts';
import type { ExtensionManifest } from '@nontara/server';

export class TMDBExtension {
  private providers = new Map<string, any>();
  private manifest: ExtensionManifest;
  private logger: ConsolaInstance;
  private client: TMDB | null = null;

  constructor() {
    this.manifest = tmdbManifest;
    this.logger = createLogger(this.manifest.name);
  }

  /**
   * Initialize extension and create providers
   */
  async initialize(): Promise<void> {
    try {
      // Initialize shared TMDB client
      this.client = await createTMDBClient();
      
      if (!this.client) {
        this.logger.warn('TMDB token not configured. Providers will return empty results.');
        return;
      }

      // Initialize providers based on manifest
      for (const providerManifest of this.manifest.providers) {
        if (providerManifest.enabled !== false) {
          try {
            const ProviderClass = providerManifest.class;
            const providerInstance = new ProviderClass(this.client, providerManifest.config);
            
            this.providers.set(providerManifest.type, providerInstance);
            this.logger.debug(`Initialized provider: ${providerManifest.name}`);
          } catch (error) {
            this.logger.error(`Failed to initialize provider ${providerManifest.name}:`, error);
          }
        } else {
          this.logger.debug(`Provider ${providerManifest.name} is disabled in manifest`);
        }
      }

      this.logger.info(`Extension initialized with ${this.providers.size} providers`);
    } catch (error) {
      this.logger.error('Failed to initialize extension:', error);
      throw error;
    }
  }

  /**
   * Cleanup and dispose resources
   */
  async dispose(): Promise<void> {
    this.logger.debug('Disposing extension...');
    
    // Cleanup providers
    for (const [type, provider] of this.providers) {
      if (typeof provider.dispose === 'function') {
        try {
          await provider.dispose();
        } catch (error) {
          this.logger.error(`Failed to dispose provider ${type}:`, error);
        }
      }
    }
    
    this.providers.clear();
    this.client = null;
    
    this.logger.info('Extension disposed');
  }

  /**
   * Get extension manifest
   */
  getManifest(): ExtensionManifest {
    return this.manifest;
  }

  /**
   * Get specific provider by type
   */
  getProvider(type: string): any {
    return this.providers.get(type);
  }

  /**
   * Get all active providers
   */
  getProviders(): Map<string, any> {
    return new Map(this.providers);
  }

  /**
   * Check if extension supports provider type
   */
  supportsProvider(type: string): boolean {
    return this.providers.has(type);
  }

  /**
   * Get list of supported provider types
   */
  getSupportedTypes(): string[] {
    return Array.from(this.providers.keys());
  }
}

// Export for extension loader
export default TMDBExtension;
```

#### **2.3.3 Movie Provider Implementation**

```typescript
// extensions/tmdb-metadata/providers/movie.ts
import type { ConsolaInstance } from 'consola';
import { createLogger } from '@nontara/server';
import type { BaseMovieProvider } from '@nontara/server';
import type { MovieInfo, MovieSearchQuery, MovieSearchResult, ResolvedMovieMetadataResult, ResolvedTitleCreditsResult } from '@nontara/server';
import type { TMDB } from 'tmdb-ts';

export class TMDBMovieProvider implements BaseMovieProvider {
  private logger: ConsolaInstance;
  private client: TMDB;
  private config: Record<string, any>;

  constructor(client: TMDB, config?: Record<string, any>) {
    this.logger = createLogger('TMDBMovieProvider');
    this.client = client;
    this.config = config ?? {};
  }

  async search(query: MovieSearchQuery): Promise<MovieSearchResult> {
    try {
      const response = await this.client.search.movies({
        query: query.title,
        year: query.year ? parseInt(query.year, 10) : undefined,
      });

      return {
        results: response.results?.map(movie => ({
          id: movie.id,
          title: movie.title,
          originalTitle: movie.original_title,
          overview: movie.overview || '',
          posterPath: movie.poster_path,
          backdropPath: movie.backdrop_path,
          releaseDate: movie.release_date,
          voteAverage: movie.vote_average || 0,
          voteCount: movie.vote_count || 0,
          popularity: movie.popularity || 0,
          adult: movie.adult || false,
          originalLanguage: movie.original_language || '',
          genreIds: movie.genre_ids || [],
          mediaType: 'movie'
        })) || []
      };
    } catch (error) {
      this.logger.error('Error searching movies:', error);
      return { results: [] };
    }
  }

  async resolve(info: MovieInfo): Promise<ResolvedMovieMetadataResult> {
    // Implementasi existing logic dari resolveMovie
    // ... (copy dari current implementation)
  }

  /**
   * Resolve credits for movie (cast & crew)
   * Credits handling terintegrasi dalam movie provider
   */
  async resolveCredits(info: { movieId: string | number }): Promise<ResolvedTitleCreditsResult> {
    try {
      const tmdbId = typeof info.movieId === 'string' ? parseInt(info.movieId, 10) : info.movieId;
      
      const credits = await this.client.movies.credits(tmdbId);

      const cast = (credits.cast || []).map(c => ({
        adult: c.adult || false,
        gender: c.gender || 0,
        id: c.id,
        knownForDepartment: c.known_for_department || '',
        name: c.name,
        originalName: c.original_name || c.name,
        popularity: c.popularity || 0,
        profilePath: c.profile_path ? `https://image.tmdb.org/t/p/w185${c.profile_path}` : null,
        castId: c.cast_id || 0,
        character: c.character || '',
        creditId: c.credit_id || '',
        order: c.order || 0
      }));

      const crew = (credits.crew || []).map(c => ({
        adult: c.adult || false,
        gender: c.gender || 0,
        id: c.id,
        knownForDepartment: c.known_for_department || '',
        name: c.name,
        originalName: c.original_name || c.name,
        popularity: c.popularity || 0,
        profilePath: c.profile_path ? `https://image.tmdb.org/t/p/w185${c.profile_path}` : null,
        creditId: c.credit_id || '',
        department: c.department || '',
        job: c.job || ''
      }));

      return { cast, crew };
    } catch (error) {
      this.logger.error('Error resolving movie credits:', error);
      return null;
    }
  }
}
```

#### **2.3.4 Series Provider Implementation**

```typescript
// extensions/tmdb-metadata/providers/series.ts
import type { ConsolaInstance } from 'consola';
import { createLogger } from '@nontara/server';
import type { BaseSeriesProvider } from '@nontara/server';
import type { SeriesInfo, SeriesSearchQuery, SeriesSearchResult, ResolvedSeriesMetadataResult, ResolvedTitleCreditsResult } from '@nontara/server';
import type { TMDB } from 'tmdb-ts';

export class TMDBSeriesProvider implements BaseSeriesProvider {
  private logger: ConsolaInstance;
  private client: TMDB;
  private config: Record<string, any>;

  constructor(client: TMDB, config?: Record<string, any>) {
    this.logger = createLogger('TMDBSeriesProvider');
    this.client = client;
    this.config = config ?? {};
  }

  async search(query: SeriesSearchQuery): Promise<SeriesSearchResult> {
    try {
      const response = await this.client.search.tvShows({
        query: query.title,
        first_air_date_year: query.year ? parseInt(query.year, 10) : undefined,
      });

      return {
        results: response.results?.map(series => ({
          id: series.id,
          name: series.name,
          originalName: series.original_name,
          overview: series.overview || '',
          posterPath: series.poster_path,
          backdropPath: series.backdrop_path,
          firstAirDate: series.first_air_date,
          voteAverage: series.vote_average || 0,
          voteCount: series.vote_count || 0,
          popularity: series.popularity || 0,
          adult: series.adult || false,
          originalLanguage: series.original_language || '',
          genreIds: series.genre_ids || [],
          mediaType: 'tv',
          originCountry: series.origin_country || []
        })) || []
      };
    } catch (error) {
      this.logger.error('Error searching series:', error);
      return { results: [] };
    }
  }

  async resolve(info: SeriesInfo): Promise<ResolvedSeriesMetadataResult> {
    // TODO: Implement series resolution
    this.logger.warn('Series resolution not yet implemented');
    return null;
  }

  /**
   * Resolve credits for series (cast & crew)
   * Credits handling terintegrasi dalam series provider
   */
  async resolveCredits(info: { seriesId: string | number }): Promise<ResolvedTitleCreditsResult> {
    try {
      const tmdbId = typeof info.seriesId === 'string' ? parseInt(info.seriesId, 10) : info.seriesId;
      
      const credits = await this.client.tvShows.credits(tmdbId);

      const cast = (credits.cast || []).map(c => ({
        adult: c.adult || false,
        gender: c.gender || 0,
        id: c.id,
        knownForDepartment: c.known_for_department || '',
        name: c.name,
        originalName: c.original_name || c.name,
        popularity: c.popularity || 0,
        profilePath: c.profile_path ? `https://image.tmdb.org/t/p/w185${c.profile_path}` : null,
        castId: c.cast_id || 0,
        character: c.character || '',
        creditId: c.credit_id || '',
        order: c.order || 0
      }));

      const crew = (credits.crew || []).map(c => ({
        adult: c.adult || false,
        gender: c.gender || 0,
        id: c.id,
        knownForDepartment: c.known_for_department || '',
        name: c.name,
        originalName: c.original_name || c.name,
        popularity: c.popularity || 0,
        profilePath: c.profile_path ? `https://image.tmdb.org/t/p/w185${c.profile_path}` : null,
        creditId: c.credit_id || '',
        department: c.department || '',
        job: c.job || ''
      }));

      return { cast, crew };
    } catch (error) {
      this.logger.error('Error resolving series credits:', error);
      return null;
    }
  }
}
```

#### **2.3.5 Person Provider Implementation**

```typescript
// extensions/tmdb-metadata/providers/person.ts
import type { ConsolaInstance } from 'consola';
import { createLogger } from '@nontara/server';
import type { BasePersonProvider } from '@nontara/server';
import type { PersonInfo, PersonSearchQuery, PersonSearchResult, ResolvedPersonResult } from '@nontara/server';
import type { TMDB } from 'tmdb-ts';

export class TMDBPersonProvider implements BasePersonProvider {
  private logger: ConsolaInstance;
  private client: TMDB;

  constructor(client: TMDB, config?: Record<string, any>) {
    this.logger = createLogger('TMDBPersonProvider');
    this.client = client;
  }

  async search(query: PersonSearchQuery): Promise<PersonSearchResult> {
    try {
      const response = await this.client.search.people({
        query: query.name,
      });

      return {
        results: response.results?.map(person => ({
          id: person.id,
          name: person.name,
          knownForDepartment: person.known_for_department || '',
          profilePath: person.profile_path,
          popularity: person.popularity || 0,
          adult: person.adult || false,
          gender: person.gender || 0,
          knownFor: person.known_for || []
        })) || []
      };
    } catch (error) {
      this.logger.error('Error searching people:', error);
      return { results: [] };
    }
  }

  async resolve(info: PersonInfo): Promise<ResolvedPersonResult> {
    try {
      const personDetails = await this.client.people.details(info.personId);

      return {
        id: personDetails.id,
        name: personDetails.name,
        biography: personDetails.biography || '',
        birthday: personDetails.birthday || null,
        deathday: personDetails.deathday || null,
        placeOfBirth: personDetails.place_of_birth || null,
        profilePath: personDetails.profile_path 
          ? `https://image.tmdb.org/t/p/w500${personDetails.profile_path}` 
          : null,
        knownForDepartment: personDetails.known_for_department || '',
        popularity: personDetails.popularity || 0,
        imdbId: personDetails.imdb_id || null,
        homepage: personDetails.homepage || null
      };
    } catch (error) {
      this.logger.error('Error resolving person:', error);
      return null;
    }
  }
}
```

#### **2.3.6 Shared TMDB Client**

```typescript
// extensions/tmdb-metadata/shared/client.ts
import { TMDB } from 'tmdb-ts';

/**
 * Create and configure TMDB client
 */
export async function createTMDBClient(): Promise<TMDB | null> {
  const token = process.env.TMDB_TOKEN;
  
  if (!token) {
    return null;
  }

  return new TMDB(token);
}
```

### 2.4 Extension Loader Integration

#### **2.4.1 Enhanced Extension Loader**

```typescript
// apps/server/src/services/extensions/loader.ts
import type { ExtensionManifest } from '@/providers/manifest';
import { MetadataProviderRegistry } from '@/providers/registry';

export class ExtensionLoader {
  private registry: MetadataProviderRegistry;
  private extensions = new Map<string, any>();
  private logger: ConsolaInstance;

  constructor() {
    this.registry = new MetadataProviderRegistry();
    this.logger = createLogger('ExtensionLoader');
  }

  /**
   * Load extension dengan manifest support
   */
  async loadExtension(extensionName: string): Promise<void> {
    try {
      const extensionPath = `@extensions/${extensionName}`;
      const extensionModule = await import(extensionPath);

      // Support manifest-based extensions
      if (extensionModule.default) {
        const ExtensionClass = extensionModule.default;
        const extension = new ExtensionClass();

        // Initialize extension
        await extension.initialize();

        // Get manifest
        const manifest: ExtensionManifest = extension.getManifest();
        
        this.logger.info(`Loading extension: ${manifest.name} v${manifest.version}`);

        // Register extension and its providers
        await this.registerExtensionFromManifest(extension, manifest);

        // Store extension instance
        this.extensions.set(manifest.name, extension);

        this.logger.success(`Extension ${manifest.name} loaded successfully`);
      }
    } catch (error) {
      this.logger.error(`Failed to load extension ${extensionName}:`, error);
      throw error;
    }
  }

  /**
   * Register providers dari extension manifest
   */
  private async registerExtensionFromManifest(
    extension: any,
    manifest: ExtensionManifest
  ): Promise<void> {
    const extensionProviders = new Map<string, any>();

    for (const providerManifest of manifest.providers) {
      if (providerManifest.enabled !== false) {
        const provider = extension.getProvider(providerManifest.type);

        if (provider) {
          const providerKey = `${manifest.name}.${providerManifest.type}`;

          // Register provider berdasarkan type
          switch (providerManifest.type) {
            case 'movie':
              this.registry.registerMovieProvider(providerKey, provider);
              break;
            case 'series':
              this.registry.registerSeriesProvider(providerKey, provider);
              break;
            case 'person':
              this.registry.registerPersonProvider(providerKey, provider);
              break;
          }

          extensionProviders.set(providerManifest.type, provider);
          
          this.logger.debug(`Registered provider: ${providerKey}`);
        }
      }
    }

    // Register extension providers in registry
    this.registry.registerExtension(manifest.name, extensionProviders);
  }

  /**
   * Unload extension
   */
  async unloadExtension(extensionName: string): Promise<void> {
    const extension = this.extensions.get(extensionName);
    
    if (extension) {
      // Dispose extension
      if (typeof extension.dispose === 'function') {
        await extension.dispose();
      }

      // Unregister from registry
      this.registry.unregisterExtension(extensionName);

      // Remove from extensions map
      this.extensions.delete(extensionName);

      this.logger.info(`Extension ${extensionName} unloaded`);
    }
  }

  /**
   * Get registry
   */
  getRegistry(): MetadataProviderRegistry {
    return this.registry;
  }

  /**
   * Get loaded extensions
   */
  getLoadedExtensions(): string[] {
    return Array.from(this.extensions.keys());
  }
}
```

### 2.5 Usage Examples

#### **2.5.1 Using Registry to Get Providers**

```typescript
// Get all movie providers
const movieProviders = registry.getMovieProviders();

// Use first available movie provider
if (movieProviders.length > 0) {
  const provider = movieProviders[0];
  const results = await provider.search({ title: 'Inception', year: '2010' });
}

// Get specific provider
const tmdbMovieProvider = registry.getMovieProvider('TMDBMetadataProvider.movie');
if (tmdbMovieProvider) {
  const movie = await tmdbMovieProvider.resolve(movieInfo);
  
  // Get credits if provider supports it
  if (tmdbMovieProvider.resolveCredits) {
    const credits = await tmdbMovieProvider.resolveCredits({ movieId: movie.id });
  }
}
```

#### **2.5.2 Checking Extension Capabilities**

```typescript
// Check if extension supports specific type
const supportsMovies = registry.extensionSupports('TMDBMetadataProvider', 'movie');
const supportsSeries = registry.extensionSupports('TMDBMetadataProvider', 'series');

console.log(`Supports movies: ${supportsMovies}`);
console.log(`Supports series: ${supportsSeries}`);

// Get all providers from extension
const tmdbProviders = registry.getProvidersByExtension('TMDBMetadataProvider');
console.log('TMDB provides:', Array.from(tmdbProviders.keys()));
```

#### **2.5.3 Using Credits from Movie/Series Providers**

```typescript
// Get movie provider
const movieProvider = registry.getMovieProvider('TMDBMetadataProvider.movie');

if (movieProvider) {
  // Resolve movie
  const movie = await movieProvider.resolve(movieInfo);
  
  // Get credits (integrated in movie provider)
  if (movieProvider.resolveCredits) {
    const credits = await movieProvider.resolveCredits({ movieId: movie.id });
    console.log(`Cast: ${credits.cast.length}, Crew: ${credits.crew.length}`);
  }
}

// Similar for series
const seriesProvider = registry.getSeriesProvider('TMDBMetadataProvider.series');

if (seriesProvider) {
  const series = await seriesProvider.resolve(seriesInfo);
  
  // Get credits (integrated in series provider)
  if (seriesProvider.resolveCredits) {
    const credits = await seriesProvider.resolveCredits({ seriesId: series.id });
  }
}
```

---

## 3. Benefits

### 3.1 Modularity
- ✅ Setiap tipe konten memiliki implementasi terpisah
- ✅ Mudah menambah tipe konten baru
- ✅ Testing dapat dilakukan per provider
- ✅ Code splitting dan lazy loading providers

### 3.2 Flexibility
- ✅ Satu extension dapat menyediakan multiple providers
- ✅ Credits handling terintegrasi dengan movie/series (tidak perlu provider terpisah)
- ✅ Provider dapat di-enable/disable secara individual via manifest
- ✅ Custom configuration per provider

### 3.3 Maintainability
- ✅ Kode lebih terorganisir dengan file structure yang jelas
- ✅ Perubahan pada satu provider tidak mempengaruhi yang lain
- ✅ Interface yang jelas untuk setiap provider type
- ✅ Manifest-based configuration (deklaratif)

### 3.4 Scalability
- ✅ Mudah menambah extension baru
- ✅ Support untuk multiple extensions dengan provider type yang sama
- ✅ Registry system untuk discovery dan management
- ✅ Resource sharing (shared TMDB client)

### 3.5 Developer Experience
- ✅ Type-safe dengan TypeScript interfaces
- ✅ Clear separation of concerns
- ✅ Consistent API across providers
- ✅ Manifest validation
- ✅ Better error handling and debugging

---

## 4. Implementation Roadmap

### Phase 1: Foundation (Week 1)
**Goal**: Setup base infrastructure

1. **Create Provider Interfaces**
   - [ ] Create `apps/server/src/providers/interfaces/movie.ts`
   - [ ] Create `apps/server/src/providers/interfaces/series.ts`
   - [ ] Create `apps/server/src/providers/interfaces/person.ts`
   - [ ] Update type exports in `@nontara/server`

2. **Create Manifest System**
   - [ ] Create `apps/server/src/providers/manifest.ts`
   - [ ] Define `ProviderManifest` interface
   - [ ] Define `ExtensionManifest` interface
   - [ ] Create manifest validation utilities

3. **Create Provider Registry**
   - [ ] Implement `MetadataProviderRegistry` class
   - [ ] Add registration methods for each provider type
   - [ ] Add query methods for providers
   - [ ] Add extension tracking

**Deliverables**:
- Base interfaces and types
- Manifest system
- Provider registry

### Phase 2: Extension Refactoring (Week 2)
**Goal**: Refactor TMDB extension to use new architecture

1. **Create New TMDB Structure**
   - [ ] Create `extensions/tmdb-metadata/manifest.ts`
   - [ ] Create `extensions/tmdb-metadata/shared/client.ts`
   - [ ] Update `extensions/tmdb-metadata/index.ts` (main class)

2. **Implement Modular Providers**
   - [ ] Implement `TMDBMovieProvider` with credits support
   - [ ] Implement `TMDBSeriesProvider` with credits support
   - [ ] Implement `TMDBPersonProvider`
   - [ ] Add proper error handling and logging

3. **Testing**
   - [ ] Unit tests for each provider
   - [ ] Integration tests for extension loading
   - [ ] Test credits integration in movie/series providers

**Deliverables**:
- Refactored TMDB extension
- Modular provider implementations
- Test coverage

### Phase 3: Extension Loader Integration (Week 3)
**Goal**: Update extension loader to support manifest system

1. **Update Extension Loader**
   - [ ] Update `ExtensionLoader` to support manifest-based extensions
   - [ ] Implement `registerExtensionFromManifest` method
   - [ ] Add extension lifecycle management (load/unload)
   - [ ] Add error handling and recovery

2. **Update Extension Service**
   - [ ] Integrate new `ExtensionLoader` with `ExtensionService`
   - [ ] Update extension discovery
   - [ ] Add registry access methods

3. **Testing**
   - [ ] Test extension loading
   - [ ] Test provider registration
   - [ ] Test extension unloading

**Deliverables**:
- Updated extension loader
- Integration with existing services
- Comprehensive tests

### Phase 4: Migration & Cleanup (Week 4)
**Goal**: Migrate existing code and cleanup

1. **Update Client Code**
   - [ ] Update services to use new registry system
   - [ ] Update API endpoints if needed
   - [ ] Add fallback mechanisms

2. **Deprecation & Cleanup**
   - [ ] Mark old `MetadataProvider` as deprecated
   - [ ] Update documentation
   - [ ] Remove old code after migration

3. **Final Testing**
   - [ ] End-to-end testing
   - [ ] Performance testing
   - [ ] Load testing with multiple extensions

**Deliverables**:
- Fully migrated codebase
- Updated documentation
- Production-ready system

---

## 5. Migration Guide

### 5.1 For Extension Developers

#### Before (Old System):
```typescript
// Single monolithic class
export default class TMDBMetadataProviderExtension extends MetadataProvider {
  async resolveMovie(info) { /* ... */ }
  async resolveSeries(info) { /* ... */ }
  async resolveTitleCredits(info) { /* ... */ }
  async resolvePeople(info) { /* ... */ }
}
```

#### After (New System):
```typescript
// 1. Create manifest
export const manifest: ExtensionManifest = {
  name: "TMDBMetadataProvider",
  version: "1.0.0",
  providers: [
    { type: "movie", class: TMDBMovieProvider, name: "TMDBMovieProvider" },
    { type: "series", class: TMDBSeriesProvider, name: "TMDBSeriesProvider" },
    { type: "person", class: TMDBPersonProvider, name: "TMDBPersonProvider" }
  ]
};

// 2. Create extension class
export class TMDBExtension {
  async initialize() { /* setup providers */ }
  getManifest() { return manifest; }
  getProvider(type) { /* return provider */ }
}

// 3. Implement individual providers
export class TMDBMovieProvider implements BaseMovieProvider {
  async search(query) { /* ... */ }
  async resolve(info) { /* ... */ }
  async resolveCredits(info) { /* credits integrated */ }
}
```

### 5.2 For Service Consumers

#### Before:
```typescript
// Get single provider
const provider = extensionService.getMetadataProvider('TMDBMetadataProvider');
await provider.resolveMovie(movieInfo);
```

#### After:
```typescript
// Get specific provider type from registry
const registry = extensionService.getRegistry();
const movieProvider = registry.getMovieProvider('TMDBMetadataProvider.movie');
await movieProvider.resolve(movieInfo);

// Or get all providers of type
const movieProviders = registry.getMovieProviders();
for (const provider of movieProviders) {
  const result = await provider.search(query);
}
```

---

## 6. Architectural Decisions

### 6.1 Why Credits are Integrated with Movie/Series Providers?

**Decision**: Credits tidak memiliki provider terpisah, melainkan terintegrasi sebagai optional method dalam movie dan series providers.

**Rationale**:
1. **Domain Cohesion**: Credits selalu terkait dengan movie atau series tertentu, bukan entitas independen
2. **API Efficiency**: TMDB dan API metadata lainnya biasanya menyediakan credits dalam konteks movie/series
3. **Simpler Architecture**: Menghindari kompleksitas tambahan dari provider terpisah
4. **Flexibility**: Optional method memungkinkan providers yang tidak support credits
5. **Performance**: Mengurangi overhead dari provider management

**Implementation**:
```typescript
// Movie provider dengan credits support
interface BaseMovieProvider {
  resolve(info: MovieInfo): Promise<ResolvedMovieMetadataResult>;
  resolveCredits?(info: { movieId: string | number }): Promise<ResolvedTitleCreditsResult>;
}

// Usage
const movieProvider = registry.getMovieProvider('TMDBMetadataProvider.movie');
const movie = await movieProvider.resolve(movieInfo);

// Check if credits supported
if (movieProvider.resolveCredits) {
  const credits = await movieProvider.resolveCredits({ movieId: movie.id });
}
```

### 6.2 Why Manifest-Based Configuration?

**Decision**: Extensions menggunakan manifest file untuk deklarasi providers.

**Rationale**:
1. **Declarative**: Clear declaration of what providers extension provides
2. **Configuration**: Enable/disable providers tanpa code changes
3. **Validation**: TypeScript interfaces ensure correctness
4. **Discoverability**: Easy to see what extension provides
5. **Metadata**: Additional information untuk debugging dan documentation

### 6.3 Why Provider Registry?

**Decision**: Centralized registry untuk provider discovery dan management.

**Rationale**:
1. **Decoupling**: Client code tidak perlu tahu tentang extensions
2. **Discovery**: Dynamic discovery of available providers
3. **Flexibility**: Support multiple providers per type
4. **Management**: Centralized lifecycle management
5. **Testing**: Easy mocking dan testing

---

## 7. Future Enhancements

### 7.1 Provider Priority System
```typescript
interface ProviderManifest {
  // ... existing fields
  priority?: number; // Higher priority providers used first
}
```

### 7.2 Provider Fallback Chain
```typescript
// Automatic fallback jika primary provider fails
const result = await registry.resolveWithFallback('movie', movieInfo, {
  preferredProviders: ['TMDBMetadataProvider.movie', 'OMDBMetadataProvider.movie']
});
```

### 7.3 Provider Health Monitoring
```typescript
// Monitor provider health and auto-disable unhealthy providers
registry.onProviderError((provider, error) => {
  if (error.isRepeated) {
    registry.disableProvider(provider);
  }
});
```

### 7.4 Provider Caching
```typescript
interface ProviderManifest {
  // ... existing fields
  config?: {
    cacheEnabled?: boolean;
    cacheTTL?: number;
  };
}
```

### 7.5 Hot Reload Support
```typescript
// Reload extension tanpa restart server
await loader.reloadExtension('TMDBMetadataProvider');
```

---

## 8. Conclusion

Proposal ini menyediakan solusi komprehensif untuk mengubah arsitektur MetadataProvider dari sistem monolitik menjadi arsitektur modular dengan manifest system. Perubahan ini akan memberikan:

1. **Better Code Organization**: Modular structure dengan clear separation of concerns
2. **Enhanced Flexibility**: Manifest-based configuration dan multiple providers per extension
3. **Improved Maintainability**: Easier to test, debug, dan extend
4. **Scalability**: Foundation untuk growth masa depan
5. **Better Developer Experience**: Clear APIs dan type safety

### Key Features Summary

✅ **Extensions Manifest System**: Deklaratif configuration untuk providers  
✅ **Modular Providers**: Separated by content type (movie, series, person)  
✅ **Integrated Credits**: Credits handling dalam movie/series providers  
✅ **Provider Registry**: Centralized management dan discovery  
✅ **Type Safety**: Full TypeScript support dengan interfaces  
✅ **Backward Compatible**: Gradual migration path  

### Success Metrics

- [ ] All existing functionality preserved
- [ ] Code coverage > 80%
- [ ] No performance degradation
- [ ] Documentation complete
- [ ] Migration guide available
- [ ] All tests passing

---

**Next Steps**: Review proposal dan approve untuk implementation.
