export interface Game {
  id: string;
  title: string;
  system: string;
  size: string;
  coverUrl: string;
}

export interface Platform {
  id: number;
  name: string;
  slug: string;
  romCount: number;
  iconUrl?: string;
}

export interface RAAchievement {
  ra_id: number | null;
  title: string | null;
  description: string | null;
  points: number | null;
  num_awarded: number | null;
  num_awarded_hardcore: number | null;
  badge_id: string | null;
}

export interface HLTBMetadata {
  main_story: number;
  main_plus_extra: number;
  completionist: number;
  all_styles?: number;
  main_story_count?: number;
  main_plus_extra_count?: number;
  completionist_count?: number;
  all_styles_count?: number;
  image?: string;
}

export interface RomRAMetadata {
  first_release_date?: number | null;
  genres?: string[];
  companies?: string[];
  achievements: RAAchievement[];
}

export interface EarnedAchievement {
  id: string;
  date: string;
  date_hardcore?: string;
}

export interface RAProgressionResult {
  rom_ra_id: number | null;
  max_possible: number | null;
  num_awarded: number | null;
  num_awarded_hardcore: number | null;
  most_recent_awarded_date?: string | null;
  earned_achievements: EarnedAchievement[];
}

export interface RAProgression {
  total: number;
  results: RAProgressionResult[];
}

export interface DetailedGame extends Game {
  summary?: string;
  developers?: string[];
  publishers?: string[];
  genres?: string[];
  franchises?: string[]; // New field
  releaseDate?: string;
  screenshots?: string[];
  fsName?: string;
  regions?: string[];
  players?: string;
  rating?: number; // New field
  esrbRating?: string;
  ratingIconUrl?: string; // Icon for Age Rating
  sha1?: string;
  collections?: string[];
  favorite?: boolean;
  manualUrl?: string;
  platformId?: number;
  videoUrl?: string; // Direct video link
  youtubeId?: string; // YouTube ID for embed
  ra_id?: number;
  merged_ra_metadata?: RomRAMetadata;
  hltb_id?: number;
  hltb_metadata?: HLTBMetadata;
  notes?: RomNote[];
  saves?: RomSave[];
  states?: RomState[];
}

export interface RomSave {
  id: number;
  file_name: string;
  file_size_bytes: number;
  emulator: string;
  slot?: number;
  created_at: string;
  updated_at: string;
}

export interface RomState {
  id: number;
  file_name: string;
  emulator: string;
  created_at: string;
  updated_at: string;
}

export interface RomNote {
  id: number;
  title: string;
  content: string;
  is_public: boolean;
  tags?: string[];
  username: string;
  created_at: string;
  updated_at: string;
}

export interface RommCollection {
  id: string;
  name: string;
  is_favorite?: boolean;
  ownerRole: 'admin' | 'user';
  games: Game[];
  coverUrl?: string; // RomM collections can have intrinsic covers
}

export interface UserProfile {
  id: string;
  username: string;
  avatarUrl?: string;
  roleName: string;
  ra_username?: string | null;
  ra_progression?: RAProgression | null;
}

// Function to safely extract base URL
const getBaseUrl = () => {
  // During development, if we're on localhost, we prefer relative paths to leverage the Vite proxy and avoid CORS.
  if (import.meta.env.DEV && window.location.hostname === 'localhost') {
    return '';
  }
  const rawUrl = import.meta.env.VITE_ROMM_BASE_URL || 'http://localhost:8080';
  const cleanUrl = rawUrl.endsWith('/') ? rawUrl.slice(0, -1) : rawUrl;
  return cleanUrl;
};

// Function to handle external URLs directly mapped from SteamGrid or IGDB dynamically
const getFullImageUrl = (urlPath: string | undefined): string | undefined => {
  if (!urlPath) return undefined;
  if (urlPath.startsWith('http://') || urlPath.startsWith('https://') || urlPath.startsWith('//')) {
    return urlPath;
  }
  const base = getBaseUrl();
  if (urlPath.startsWith('/') && base.endsWith('/')) {
    return `${base.slice(0, -1)}${urlPath}`;
  } else if (!urlPath.startsWith('/') && !base.endsWith('/')) {
    return `${base}/${urlPath}`;
  }
  return `${base}${urlPath}`;
};

// Map RomM's native spec to our simplified app interface
const mapRomToGame = (apiRom: any): Game => {
  return {
    id: String(apiRom.id),
    title: apiRom.name || apiRom.fs_name,
    system: apiRom.platform_display_name || apiRom.platform_slug || 'Unknown Platform',
    size: formatBytes(apiRom.fs_size_bytes || 0),
    coverUrl: getFullImageUrl(apiRom.url_cover) || 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?auto=format&fit=crop&q=80&w=400',
  };
};

const formatBytes = (bytes: number) => {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};

const formatDate = (unixSeconds: any) => {
  if (!unixSeconds) return undefined;
  const val = Number(unixSeconds);
  if (isNaN(val) || val < 1000000) return String(unixSeconds); 
  const date = new Date(val * 1000);
  return new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date);
};

export const rommApiClient = {
  get apiBase() {
    const cleanUrl = getBaseUrl();
    if (cleanUrl === '') return '/api';
    return cleanUrl.endsWith('/api') ? cleanUrl : `${cleanUrl}/api`;
  },

  get token() {
    return localStorage.getItem('romm_token');
  },

  get headers() {
    return {
      'Authorization': `Bearer ${this.token}`,
      'Accept': 'application/json'
    };
  },

  async login(username: string, password: string): Promise<any> {
    const data = new URLSearchParams();
    data.append('username', username);
    data.append('password', password);
    data.append('grant_type', 'password');
    // Ensure we request the explicit permissions RomM FastAPI needs
    data.append('scope', 'me.read roms.read roms.user.write collections.read collections.write assets.read platforms.read');

    const res = await fetch(`${this.apiBase}/token`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: data
    });

    if (!res.ok) throw new Error('Authentication failed');
    const json = await res.json();
    localStorage.setItem('romm_token', json.access_token);
    return json;
  },

  logout() {
    localStorage.removeItem('romm_token');
  },

  async fetchGames(): Promise<Game[]> {
    // We use this as a stand-in for 'Continue Playing'. Fetching last played games.
    const res = await fetch(`${this.apiBase}/roms?last_played=true&limit=10`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch last played network data.');
    const json = await res.json();
    return json.items ? json.items.map(mapRomToGame) : [];
  },

  async fetchRecentGames(): Promise<Game[]> {
    const res = await fetch(`${this.apiBase}/roms?limit=50&order_by=id&order_dir=desc`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch recents.');
    const json = await res.json();
    return json.items ? json.items.map(mapRomToGame) : [];
  },

  async fetchFavorites(): Promise<Game[]> {
    const res = await fetch(`${this.apiBase}/roms?favorite=true&limit=50`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch favorites.');
    const json = await res.json();
    return json.items ? json.items.map(mapRomToGame) : [];
  },

  async fetchStats(): Promise<any> {
    const res = await fetch(`${this.apiBase}/stats`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch stats.');
    return res.json();
  },

  async fetchPlatforms(): Promise<Platform[]> {
    const res = await fetch(`${this.apiBase}/platforms`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch platforms.');
    const json = await res.json();
    return json.map((p: any) => ({
      id: p.id,
      name: p.display_name || p.name || p.fs_slug,
      slug: p.fs_slug,
      romCount: p.rom_count || 0,
      iconUrl: getFullImageUrl(p.url_logo)
    }));
  },

  async fetchGamesByPlatform(platformId: number): Promise<Game[]> {
    const res = await fetch(`${this.apiBase}/roms?platform_ids=${platformId}&limit=100`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch platform games.');
    const json = await res.json();
    return json.items ? json.items.map(mapRomToGame) : [];
  },

  async fetchGameDetails(gameId: string): Promise<DetailedGame> {
    const res = await fetch(`${this.apiBase}/roms/${gameId}`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch game details.');
    const json = await res.json();
    const game = mapRomToGame(json);
    
    // Extract metadata with fallbacks across all providers
    const providers = [
      json.latest_igdb_metadata, json.igdb_metadata,
      json.latest_moby_metadata, json.moby_metadata,
      json.latest_screenscraper_metadata, json.screenscraper_metadata,
      json.latest_launchbox_metadata, json.launchbox_metadata,
      json.latest_steamgrid_metadata, json.steamgrid_metadata
    ].filter(Boolean);

    const getFirst = (key: string) => {
      for (const p of providers) {
        if (p[key] !== undefined && p[key] !== null) return p[key];
      }
      return json[key];
    };

    const getAll = (key: string) => {
      const all: any[] = [];
      if (Array.isArray(json[key])) all.push(...json[key]);
      for (const p of providers) {
        if (Array.isArray(p[key])) all.push(...p[key]);
      }
      return Array.from(new Set(all)).filter(Boolean);
    };

    const sanitize = (val: any): string => {
      if (typeof val !== 'string') return String(val?.name || val?.display_name || val || '');
      return val.trim().replace(/;+$/, '');
    };

    // Extract Age Ratings from all providers (array format for IGDB/ScreenScraper)
    const ageRatings = new Set<string>();
    for (const p of providers) {
      if (Array.isArray(p.age_ratings)) {
        p.age_ratings.forEach((ar: any) => {
          if (ar.rating) ageRatings.add(String(ar.rating));
        });
      }
      const pRating = p.esrb_rating || p.esrb || p.pegi_rating || p.pegi;
      if (pRating && typeof pRating === 'string') ageRatings.add(pRating);
    }
    
    // Filter out placeholders like "Not Rated" or "Unknown" if we have valid ratings
    let finalRatings = Array.from(ageRatings).filter(Boolean);
    const placeholders = ['not rated', 'unknown', 'pending', 'nr', 'rp', 'rating pending'];
    if (finalRatings.some(r => !placeholders.includes(r.toLowerCase()))) {
      finalRatings = finalRatings.filter(r => !placeholders.includes(r.toLowerCase()));
    }
    
    const ageRatingStr = finalRatings.join(' / ') || (json.esrb_rating || json.esrb);

    // Defensive mapping for regions
    const regionsRaw = [
      ...(json.rom_regions || []),
      ...(json.region ? [json.region] : []),
      ...(getFirst('regions') || [])
    ];
    
    const regions = Array.from(new Set(regionsRaw.map((r: any) => 
      typeof r === 'string' ? r : (r.display_name || r.name || String(r))
    ))).filter(Boolean);

    const extractCompanies = (type: 'developer' | 'publisher') => {
      const companies = new Set<string>();
      
      // 1. Root lists
      const rootList = type === 'developer' ? (json.developers || json.companies) : (json.publishers || json.companies);
      if (Array.isArray(rootList)) {
        rootList.forEach(c => companies.add(sanitize(c)));
      }

      // 2. Provider metadata
      for (const p of providers) {
        // IGDB involved_companies
        if (Array.isArray(p.involved_companies)) {
          p.involved_companies.forEach((ic: any) => {
            if (ic[type] && ic.company?.name) companies.add(sanitize(ic.company.name));
          });
        }
        
        // General companies/developers/publishers lists
        const pList = p.companies || (type === 'developer' ? (p.developers || p.developer_companies) : (p.publishers || p.publisher_companies));
        if (Array.isArray(pList)) {
          pList.forEach((c: any) => companies.add(sanitize(c)));
        }
      }

      // 3. Root singulars
      const rootSingular = type === 'developer' ? (json.developer || json.developer_name) : (json.publisher || json.publisher_name);
      if (rootSingular && typeof rootSingular === 'string') companies.add(sanitize(rootSingular));

      return Array.from(companies).filter(Boolean);
    };

    const devList = extractCompanies('developer');
    const pubList = extractCompanies('publisher');

    // Rating extraction with ScreenScraper/Launchbox fallbacks
    let ratingVal = getFirst('rating') || getFirst('total_rating') || getFirst('aggregated_rating') || getFirst('ss_score') || getFirst('community_rating');
    if (ratingVal && String(ratingVal).includes('.')) {
      const parsed = parseFloat(String(ratingVal));
      // Normalize 0-10 or 0-5 scales to 100%
      if (parsed <= 10) ratingVal = parsed * 10;
      else if (parsed <= 5) ratingVal = parsed * 20;
    }

    return {
      ...game,
      summary: json.summary || getFirst('summary') || getFirst('description'),
      developers: devList,
      publishers: pubList,
      genres: getAll('genres').map(sanitize).filter(Boolean),
      franchises: getAll('franchises').map(sanitize).filter(Boolean),
      releaseDate: formatDate(json.release_date || getFirst('first_release_date') || getFirst('release_date')),
      fsName: json.fs_name || json.file || json.fs_path,
      regions: regions.length > 0 ? regions : ['Global'],
      players: (() => {
        let val = json.metadatum?.player_count || json.players || getFirst('player_count') || getFirst('total_players') || getFirst('players') || getFirst('max_players');
        if (val && (String(val) === '1' || String(val).toLowerCase() === 'single player')) {
          const modes = getAll('game_modes').map(sanitize).map(m => m.toLowerCase());
          if (modes.includes('multiplayer') || modes.includes('co-operative') || modes.includes('split screen')) {
            val = 'Multiplayer';
          }
        }
        if (val) {
          val = String(val);
          if (!val.toLowerCase().includes('player')) {
            val = `${val} ${val === '1' ? 'Player' : 'Players'}`;
          }
        }
        return val;
      })(),
      rating: ratingVal ? Math.round(Number(ratingVal)) : undefined,
      esrbRating: ageRatingStr,
      sha1: json.hashes?.sha1,
      collections: Array.from(new Set([
        ...(json.rom_collections || []).map((c: any) => c.name),
        ...(getFirst('collections') || [])
      ])).filter(Boolean),
      favorite: (json.user_collections || json.rom_collections || []).some((c: any) => c.is_favorite || c.name === 'Favorites'),
      manualUrl: getFullImageUrl(json.url_manual),
      platformId: json.platform_id,
      videoUrl: getFullImageUrl(json.url_video || json.ss_metadata?.video_url),
      youtubeId: json.youtube_video_id || json.igdb_metadata?.youtube_video_id || json.launchbox_metadata?.youtube_video_id,
      ra_id: json.ra_id,
      merged_ra_metadata: json.merged_ra_metadata,
      hltb_id: json.hltb_id,
      hltb_metadata: json.hltb_metadata,
      screenshots: Array.from(new Set([
        ...(json.merged_screenshots || []).map((s: string) => getFullImageUrl(s)),
        ...(json.screenshots || []).map((s: any) => s.url ? getFullImageUrl(s.url) || '' : ''),
        ...(getFirst('screenshots') || [])
      ])).filter(Boolean) as string[],
      notes: json.all_user_notes || [],
      saves: json.user_saves || [],
      states: json.user_states || [],
    };
  },

  async fetchCollections(): Promise<RommCollection[]> {
    const res = await fetch(`${this.apiBase}/collections`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch collections meta.');
    const collections = await res.json();
    
    // Concurrently fetch the games arrays for each populated collection to supply UI with hydration
    const mapped = await Promise.all(collections.map(async (c: any) => {
      let gamesItems: Game[] = [];
      try {
        const gamesRes = await fetch(`${this.apiBase}/roms?collection_id=${c.id}&limit=20`, { headers: this.headers });
        if (gamesRes.ok) {
          const gamesJson = await gamesRes.json();
          gamesItems = gamesJson.items ? gamesJson.items.map(mapRomToGame) : [];
        }
      } catch (e) { console.error("Could not fetch collection games", e) }

      const coverUrl = c.url_cover ? getFullImageUrl(c.url_cover) : undefined;

      // Currently extrapolating that public featured collections represent admin
      let role: "admin" | "user" = "user";
      if (c.name.toLowerCase() === 'featured' && c.is_public) {
        role = "admin";
      }

      return {
        id: String(c.id),
        name: c.name,
        is_favorite: c.is_favorite || false,
        ownerRole: role,
        games: gamesItems,
        coverUrl
      };
    }));

    return mapped;
  },

  async fetchCollectionDetails(collectionId: string): Promise<any> {
    const res = await fetch(`${this.apiBase}/collections/${collectionId}`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch collection details.');
    return res.json();
  },

  async updateCollection(collectionId: string, data: { name: string; description?: string; rom_ids: number[] }): Promise<any> {
    const formData = new FormData();
    formData.append('name', data.name);
    formData.append('description', data.description || '');
    formData.append('rom_ids', JSON.stringify(data.rom_ids));

    const res = await fetch(`${this.apiBase}/collections/${collectionId}?is_public=false&remove_cover=false`, {
      method: 'PUT',
      headers: this.headers,
      body: formData
    });
    if (!res.ok) throw new Error('Failed to update collection.');
    return res.json();
  },

  async toggleFavorite(gameId: string, favorite: boolean): Promise<any> {
    // 1. Find the Favorites collection
    const collections = await this.fetchCollections();
    const favCol = collections.find(c => c.is_favorite);
    if (!favCol) throw new Error("Could not find Favorites collection");

    // 2. Fetch current member IDs (full list for collection-tier update)
    const details = await this.fetchCollectionDetails(favCol.id);
    let ids: number[] = details.rom_ids || [];

    // 3. Toggle membership
    const numId = parseInt(gameId);
    if (favorite) {
      if (!ids.includes(numId)) ids.push(numId);
    } else {
      ids = ids.filter(id => id !== numId);
    }

    // 4. Persistence via Multipart-Encoded PUT
    return this.updateCollection(favCol.id, { 
      name: favCol.name || 'Favorites',
      rom_ids: ids 
    });
  },

  async createCollection(name: string, description: string = ''): Promise<any> {
    const formData = new FormData();
    formData.append('name', name);
    formData.append('description', description);
    formData.append('rom_ids', '[]'); // Start empty

    const res = await fetch(`${this.apiBase}/collections?is_public=false`, {
      method: 'POST',
      headers: this.headers,
      body: formData
    });
    if (!res.ok) throw new Error('Failed to create collection.');
    return res.json();
  },

  getPlayUrl(gameId: string): string {
    return `/rom/${gameId}/ejs`;
  },

  getManualUrl(gameId: string, platformId?: number): string {
    // Constructed manual URL: /assets/romm/resources/roms/[platform_id]/[rom_id]/manual/[rom_id].pdf
    // We return a relative path to ensure the request goes through our local origin (bypassing CORS via Vite proxy).
    if (platformId) {
      return `/assets/romm/resources/roms/${platformId}/${gameId}/manual/${gameId}.pdf`;
    }
    return `${this.apiBase}/roms/${gameId}/manual`;
  },

  async fetchCurrentUser(): Promise<UserProfile> {
    const res = await fetch(`${this.apiBase}/users/me`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch user profile.');
    const json = await res.json();
    
    // RomM obfuscates user assets using hex-encoded IDs (e.g. "User:1" -> "557365723a31")
    const hexId = Array.from(`User:${json.id}`).map(c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
    const ts = new Date().getTime(); // Auto-cache bust like official UI
    const constructedAvatarUrl = `${getBaseUrl()}/assets/romm/assets/users/${hexId}/profile/avatar.png?ts=${ts}`;

    return {
      id: String(json.id),
      username: json.username || 'Unknown',
      avatarUrl: constructedAvatarUrl,
      roleName: json.role?.role_name || json.role?.name || String(json.role) || 'User',
      ra_username: json.ra_username,
      ra_progression: json.ra_progression,
    };
  },

  async fetchRomSaves(id: string): Promise<RomSave[]> {
    const res = await fetch(`${this.apiBase}/saves?rom_id=${id}`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch saves.');
    return res.json();
  },

  async fetchRomStates(id: string): Promise<RomState[]> {
    const res = await fetch(`${this.apiBase}/states?rom_id=${id}`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch states.');
    return res.json();
  },

  async fetchRomNotes(id: string): Promise<RomNote[]> {
    const res = await fetch(`${this.apiBase}/roms/${id}/notes`, { headers: this.headers });
    if (!res.ok) throw new Error('Failed to fetch notes.');
    return res.json();
  },

  getRomFileUrl(romId: string, fileName: string = 'rom.zip'): string {
    const encodedName = fileName.split('/').map(s => encodeURIComponent(s)).join('/');
    return `${this.apiBase}/roms/${romId}/content/token/${this.token}/${encodedName}`;
  },

  getSaveFileUrl(saveId: number): string {
    return `${this.apiBase}/saves/${saveId}/content?access_token=${this.token}`;
  },

  getStateFileUrl(stateId: number): string {
    return `${this.apiBase}/states/${stateId}/content?access_token=${this.token}`;
  },

  async uploadSave(romId: string, file: Blob, emulator: string): Promise<any> {
    const formData = new FormData();
    formData.append('file', file, 'save.srm');
    formData.append('rom_id', romId);
    formData.append('emulator', emulator);

    const res = await fetch(`${this.apiBase}/saves`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Accept': 'application/json'
        // Do not set Content-Type, browser will set it with boundary
      },
      body: formData
    });
    if (!res.ok) throw new Error('Failed to upload save.');
    return res.json();
  }
};
