import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {AddonClient} from './addonClient';
import {
  AddonManifest,
  AddonStreamResult,
  CatalogDescriptor,
  ContentType,
  InstalledAddon,
  MetaDetail,
  MetaPreview,
  SubtitleTrack,
} from './addonTypes';

const ADDONS_KEY = 'addons:installed_addons';

export const DEFAULT_ADDON_URLS = [
  'https://v3-cinemeta.strem.io/manifest.json',
  'https://opensubtitles-v3.strem.io/manifest.json',
];

// Embedded fallback manifests — used when network fetch fails (e.g. Fire TV first boot)
const FALLBACK_MANIFESTS: Record<string, AddonManifest> = {
  'https://v3-cinemeta.strem.io/manifest.json': {
    id: 'com.linvo.cinemeta',
    version: '3.0.13',
    name: 'Cinemeta',
    description: 'The official add-on for movie and series catalogs',
    logo: 'https://v3-cinemeta.strem.io/images/cinemeta-logo.png',
    types: ['movie', 'series'],
    resources: [
      'catalog',
      {name: 'meta', types: ['movie', 'series'], idPrefixes: ['tt']},
      {name: 'addon_catalog', types: ['movie' as ContentType], idPrefixes: undefined},
    ],
    catalogs: [
      {type: 'movie', id: 'top', name: 'Popular', extra: [{name: 'genre'}, {name: 'skip'}]},
      {type: 'series', id: 'top', name: 'Popular', extra: [{name: 'genre'}, {name: 'skip'}]},
      {type: 'movie', id: 'year', name: 'New', extra: [{name: 'genre'}, {name: 'skip'}]},
      {type: 'series', id: 'year', name: 'New', extra: [{name: 'genre'}, {name: 'skip'}]},
      {type: 'movie', id: 'imdbRating', name: 'Featured', extra: [{name: 'genre'}, {name: 'skip'}]},
      {type: 'series', id: 'imdbRating', name: 'Featured', extra: [{name: 'genre'}, {name: 'skip'}]},
    ],
    idPrefixes: ['tt'],
  },
  'https://opensubtitles-v3.strem.io/manifest.json': {
    id: 'org.stremio.opensubtitlesv3',
    version: '1.0.0',
    name: 'OpenSubtitles v3',
    description: 'Subtitles from OpenSubtitles',
    types: ['movie', 'series'],
    resources: ['subtitles'],
    catalogs: [],
    idPrefixes: ['tt'],
  },
};

export class AddonManager {
  private addons: Map<
    string,
    {client: AddonClient; installed: InstalledAddon}
  > = new Map();
  private listeners: (() => void)[] = [];

  subscribe(listener: () => void): () => void {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  private notify() {
    this.listeners.forEach(l => l());
  }

  async load(): Promise<void> {
    const raw = await AsyncStorage.getItem(ADDONS_KEY);
    if (!raw) {
      await this.installDefaults();
      return;
    }

    try {
      const saved: InstalledAddon[] = JSON.parse(raw);
      for (const installed of saved) {
        const client = new AddonClient(
          installed.transportUrl,
          installed.manifest,
        );
        this.addons.set(installed.manifest.id, {client, installed});
      }
    } catch {
      await this.installDefaults();
    }
  }

  private async installDefaults(): Promise<void> {
    for (const url of DEFAULT_ADDON_URLS) {
      try {
        await this.installAddon(url);
      } catch (e) {
        console.warn(`Failed to fetch default addon ${url}, using fallback:`, e);
        // Use embedded fallback manifest so defaults work even without network
        const fallback = FALLBACK_MANIFESTS[url];
        if (fallback) {
          try {
            await this.saveAddon(url, fallback);
          } catch (e2) {
            console.warn(`Failed to save fallback addon ${url}:`, e2);
          }
        }
      }
    }
  }

  normalizeUrl(url: string): string {
    if (url.startsWith('stremio://')) {
      return 'https://' + url.slice('stremio://'.length);
    }
    if (url.startsWith('streamvault://addon/')) {
      return url.slice('streamvault://addon/'.length);
    }
    return url;
  }

  async installAddon(manifestUrl: string): Promise<AddonManifest> {
    const normalizedUrl = this.normalizeUrl(manifestUrl);
    const response = await axios.get(normalizedUrl, {timeout: 10000});
    const manifest = response.data as AddonManifest;
    await this.saveAddon(normalizedUrl, manifest);
    return manifest;
  }

  async saveAddon(transportUrl: string, manifest: AddonManifest): Promise<void> {
    const installed: InstalledAddon = {
      manifest,
      transportUrl,
      enabled: true,
      installedAt: Date.now(),
    };
    const client = new AddonClient(transportUrl, manifest);
    this.addons.set(manifest.id, {client, installed});
    await this.save();
    this.notify();
  }

  removeAddon(addonId: string): void {
    this.addons.delete(addonId);
    this.save();
    this.notify();
  }

  toggleAddon(addonId: string, enabled: boolean): void {
    const entry = this.addons.get(addonId);
    if (entry) {
      entry.installed.enabled = enabled;
      this.save();
      this.notify();
    }
  }

  getInstalledAddons(): InstalledAddon[] {
    return Array.from(this.addons.values())
      .map(e => e.installed)
      .sort((a, b) => a.installedAt - b.installedAt);
  }

  isInstalled(addonId: string): boolean {
    return this.addons.has(addonId);
  }

  getAllCatalogs(): {addon: AddonManifest; catalog: CatalogDescriptor}[] {
    const result: {addon: AddonManifest; catalog: CatalogDescriptor}[] = [];
    for (const {installed} of this.addons.values()) {
      if (!installed.enabled) continue;
      for (const catalog of installed.manifest.catalogs) {
        result.push({addon: installed.manifest, catalog});
      }
    }
    return result;
  }

  async getStreamsFromAll(
    type: ContentType,
    id: string,
  ): Promise<AddonStreamResult[]> {
    const tasks = Array.from(this.addons.values())
      .filter(
        ({installed, client}) =>
          installed.enabled && client.supportsResource('stream', type, id),
      )
      .map(async ({client, installed}) => {
        try {
          const response = await client.getStreams(type, id);
          return {
            addon: installed.manifest,
            streams: response.streams || [],
          } as AddonStreamResult;
        } catch {
          return null;
        }
      });

    const results = await Promise.all(tasks);
    return results.filter((r): r is AddonStreamResult => r !== null);
  }

  /**
   * Progressive stream loading — calls onResult as each addon responds.
   * Returns a cancel function. Calls onDone when all addons have finished.
   */
  streamStreamsFromAll(
    type: ContentType,
    id: string,
    onResult: (result: AddonStreamResult) => void,
    onDone: () => void,
  ): () => void {
    let cancelled = false;
    const entries = Array.from(this.addons.values()).filter(
      ({installed, client}) =>
        installed.enabled && client.supportsResource('stream', type, id),
    );

    let remaining = entries.length;
    if (remaining === 0) {
      onDone();
      return () => {};
    }

    for (const {client, installed} of entries) {
      client
        .getStreams(type, id)
        .then(response => {
          if (cancelled) return;
          const streams = response.streams || [];
          if (streams.length > 0) {
            onResult({addon: installed.manifest, streams});
          }
        })
        .catch(() => {})
        .finally(() => {
          remaining--;
          if (remaining <= 0 && !cancelled) {
            onDone();
          }
        });
    }

    return () => {
      cancelled = true;
    };
  }

  async getSubtitlesFromAll(
    type: ContentType,
    id: string,
    extra?: Record<string, string>,
  ): Promise<SubtitleTrack[]> {
    const tasks = Array.from(this.addons.values())
      .filter(
        ({installed, client}) =>
          installed.enabled && client.supportsResource('subtitles', type, id),
      )
      .map(async ({client}) => {
        try {
          const response = await client.getSubtitles(type, id, extra);
          return response.subtitles || [];
        } catch {
          return [];
        }
      });

    const results = await Promise.all(tasks);
    const all = results.flat();

    // Deduplicate: keep one subtitle per language (prefer ones with labels)
    const byLang = new Map<string, SubtitleTrack>();
    for (const sub of all) {
      const key = (sub.lang || 'unknown').toLowerCase().trim();
      if (!byLang.has(key)) {
        byLang.set(key, sub);
      } else if (sub.label && !byLang.get(key)!.label) {
        // Prefer entries that have a descriptive label
        byLang.set(key, sub);
      }
    }
    return Array.from(byLang.values());
  }

  async searchAll(
    query: string,
    type?: ContentType,
  ): Promise<MetaPreview[]> {
    const tasks: Promise<MetaPreview[]>[] = [];

    for (const {installed, client} of this.addons.values()) {
      if (!installed.enabled) continue;
      for (const catalog of installed.manifest.catalogs) {
        if (type && catalog.type !== type) continue;
        const supportsSearch = catalog.extra?.some(e => e.name === 'search');
        if (!supportsSearch) continue;

        tasks.push(
          client
            .getCatalog(catalog.type, catalog.id, {search: query})
            .then(r => r.metas || [])
            .catch(() => []),
        );
      }
    }

    const results = await Promise.all(tasks);
    const seen = new Set<string>();
    const merged: MetaPreview[] = [];

    for (const batch of results) {
      for (const item of batch) {
        if (!seen.has(item.id)) {
          seen.add(item.id);
          merged.push(item);
        }
      }
    }

    return merged;
  }

  async getMetaFromFirst(
    type: ContentType,
    id: string,
  ): Promise<MetaDetail | null> {
    for (const {installed, client} of this.addons.values()) {
      if (!installed.enabled) continue;
      if (!client.supportsResource('meta', type, id)) continue;
      try {
        const response = await client.getMeta(type, id);
        return response.meta;
      } catch {}
    }
    return null;
  }

  async getCatalog(
    addonId: string,
    type: ContentType,
    catalogId: string,
    extra?: Record<string, string>,
  ): Promise<MetaPreview[]> {
    const entry = this.addons.get(addonId);
    if (!entry || !entry.installed.enabled) return [];
    try {
      const response = await entry.client.getCatalog(type, catalogId, extra);
      return response.metas || [];
    } catch {
      return [];
    }
  }

  async save(): Promise<void> {
    const data = Array.from(this.addons.values()).map(e => e.installed);
    await AsyncStorage.setItem(ADDONS_KEY, JSON.stringify(data));
  }
}

export const addonManager = new AddonManager();
