
const http = require('http');
const fs = require('fs');
const path = require('path');
const { Readable } = require('stream');

const { loadConfig } = require('../mod/config');
const tmdb = require('../mod/tmdb');
const { listAll, loadProviders } = require('../mod/loadProviders');
const { scrapeAll } = require('../mod/scrape');
const { normalize } = require('../mod/normalize');
const { applyFilters, rankStreams } = require('../mod/filter');
const spec = require('./openapi');

const cfg = loadConfig();
const PORT = process.env.STREAM_API_PORT ? parseInt(process.env.STREAM_API_PORT, 10) : cfg.apiPort;
const HOST = process.env.STREAM_API_HOST || cfg.apiHost;
const SWAGGER_HTML = path.join(__dirname, 'public', 'index.html');

function json(res, status, obj) {
  const body = JSON.stringify(obj);
  res.writeHead(status, {
    'Content-Type': 'application/json; charset=utf-8',
    'Content-Length': Buffer.byteLength(body),
    'Access-Control-Allow-Origin': '*',
    'Cache-Control': 'no-store',
  });
  res.end(body);
}

function html(res, status, text, type = 'text/html; charset=utf-8') {
  res.writeHead(status, { 'Content-Type': type, 'Access-Control-Allow-Origin': '*', 'Cache-Control': 'no-store' });
  res.end(text);
}

const PROXY_DISALLOWED = new Set([
  'host',
  'connection',
  'content-length',
  'transfer-encoding',
  'upgrade',
  'keep-alive',
  'proxy-authorization',
  'proxy-authentication',
  'te',
  'trailer',
  'via',
  'warning',
  'expect',
  'date',
  'dnt',
  'accept-encoding',
]);

function parseProxyHeaders(raw) {
  if (!raw) return {};
  let parsed;
  try {
    parsed = JSON.parse(raw);
  } catch {
    return {};
  }
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
  const out = {};
  for (const [name, value] of Object.entries(parsed)) {
    if (typeof value !== 'string' || !value) continue;
    if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) continue;
    if (PROXY_DISALLOWED.has(name.toLowerCase())) continue;
    out[name] = value;
  }
  return out;
}

function proxyTarget(target, headers) {
  const query = new URLSearchParams({ url: target, h: JSON.stringify(headers || {}) });
  return `/proxy/media?${query.toString()}`;
}

function rewriteM3u8(text, baseUrl, headers) {
  return text
    .split(/\r?\n/)
    .map((line) => {
      const trimmed = line.trim();
      if (!trimmed) return line;
      if (trimmed.startsWith('#')) {
        return line.replace(/(URI\s*=\s*)(?:"([^"]*)"|'([^']*)')/g, (match, prefix, double, single) => {
          const uri = double !== undefined ? double : single;
          try {
            return `${prefix}"${proxyTarget(new URL(uri, baseUrl).toString(), headers)}"`;
          } catch {
            return match;
          }
        });
      }
      try {
        return proxyTarget(new URL(trimmed, baseUrl).toString(), headers);
      } catch {
        return line;
      }
    })
    .join('\n');
}

async function handleProxyMedia(req, res, url) {
  const target = (url.searchParams.get('url') || '').trim();
  const headers = parseProxyHeaders(url.searchParams.get('h'));
  let parsed;
  try {
    parsed = new URL(target);
  } catch {
    return json(res, 400, { error: 'missing or invalid ?url=' });
  }
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    return json(res, 400, { error: 'proxy only supports http/https' });
  }

  const upstreamHeaders = { ...headers };
  if (req.headers.range) upstreamHeaders.range = req.headers.range;

  let upstream;
  try {
    upstream = await fetch(parsed.toString(), { headers: upstreamHeaders, redirect: 'follow' });
  } catch (e) {
    return json(res, 502, { error: `proxy fetch failed: ${e.message}` });
  }

  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Cache-Control', 'no-store');

  const contentType = upstream.headers.get('content-type') || '';
  const looksLikeM3u8 = /mpegurl/i.test(contentType) || /\.m3u8(?:[?#].*)?$/i.test(parsed.pathname + parsed.search);
  if (looksLikeM3u8 && upstream.status < 400) {
    try {
      const text = await upstream.text();
      if (/mpegurl/i.test(contentType) || text.trimStart().startsWith('#EXTM3U')) {
        const body = rewriteM3u8(text, parsed.toString(), headers);
        res.writeHead(upstream.status, {
          'Content-Type': 'application/vnd.apple.mpegurl; charset=utf-8',
          'Content-Length': Buffer.byteLength(body),
        });
        return res.end(body);
      }
      res.writeHead(upstream.status, {
        'Content-Type': contentType || 'text/plain; charset=utf-8',
        'Content-Length': Buffer.byteLength(text),
      });
      return res.end(text);
    } catch (e) {
      if (!res.headersSent) return json(res, 502, { error: `proxy playlist failed: ${e.message}` });
      return res.destroy();
    }
  }

  res.statusCode = upstream.status;
  const copyHeaders = ['content-type', 'content-range', 'accept-ranges'];
  if (upstream.status === 206) copyHeaders.push('content-length');
  for (const name of copyHeaders) {
    const value = upstream.headers.get(name);
    if (value) res.setHeader(name, value);
  }

  if (!upstream.body) return res.end();
  const body = Readable.fromWeb(upstream.body);
  body.on('error', () => {
    try {
      upstream.body.cancel();
    } catch {}
  });
  body.pipe(res).on('error', () => {});
}

function searchResult(m) {
  const isTv = m.media_type === 'tv';
  const date = isTv ? m.first_air_date : m.release_date;
  return {
    tmdb_id: m.id,
    type: isTv ? 'tv' : 'movie',
    name: m.title || m.name,
    year: date ? String(date).slice(0, 4) : '',
    poster: m.poster_path ? `https://image.tmdb.org/t/p/w342${m.poster_path}` : '',
    backdrop: m.backdrop_path ? `https://image.tmdb.org/t/p/w1280${m.backdrop_path}` : '',
    rating: m.vote_average ? Number(m.vote_average).toFixed(1) : '',
    overview: m.overview || '',
  };
}

async function handleSearch(req, res, url) {
  const q = (url.searchParams.get('q') || '').trim();
  if (!q) return json(res, 400, { error: 'missing ?q=' });
  try {
    const results = await tmdb.search(q, cfg.tmdbKey);
    return json(res, 200, { query: q, count: results.length, results: results.map(searchResult) });
  } catch (e) {
    return json(res, 502, { error: `tmdb search failed: ${e.message}` });
  }
}

async function handleTrending(req, res, url) {
  const type = url.searchParams.get('type') || 'all';
  const timeWindow = url.searchParams.get('window') || 'week';
  if (!['all', 'movie', 'tv'].includes(type)) {
    return json(res, 400, { error: '?type= must be all, movie or tv' });
  }
  if (!['day', 'week'].includes(timeWindow)) {
    return json(res, 400, { error: '?window= must be day or week' });
  }
  try {
    const results = await tmdb.trending(type, timeWindow, cfg.tmdbKey);
    return json(res, 200, { type, window: timeWindow, count: results.length, results: results.map(searchResult) });
  } catch (e) {
    return json(res, 502, { error: `tmdb trending failed: ${e.message}` });
  }
}

async function handleSeasons(req, res, url, id) {
  try {
    const seasons = await tmdb.getSeasons(id, cfg.tmdbKey);
    return json(res, 200, {
      tmdb_id: id,
      count: seasons.length,
      seasons: seasons.map((s) => ({
        season_number: s.season_number,
        name: s.name,
        episode_count: s.episode_count,
        air_date: s.air_date || '',
      })),
    });
  } catch (e) {
    return json(res, 502, { error: `tmdb seasons failed: ${e.message}` });
  }
}

async function handleEpisodes(req, res, url, id, season) {
  try {
    const episodes = await tmdb.getEpisodes(id, season, cfg.tmdbKey);
    return json(res, 200, {
      tmdb_id: id,
      season: parseInt(season, 10),
      count: episodes.length,
      episodes: episodes.map((e) => ({
        episode_number: e.episode_number,
        name: e.name,
        air_date: e.air_date || '',
        still: e.still_path ? `https://image.tmdb.org/t/p/w300${e.still_path}` : '',
      })),
    });
  } catch (e) {
    return json(res, 502, { error: `tmdb episodes failed: ${e.message}` });
  }
}

const TMDB_IMG = 'https://image.tmdb.org/t/p/';

function fullImage(pathname) {
  return pathname ? `${TMDB_IMG}original${pathname}` : '';
}

async function handleDetails(req, res, type, id) {
  try {
    const d = await tmdb.details(type, id, cfg.tmdbKey);
    d.poster_url = fullImage(d.poster_path);
    d.backdrop_url = fullImage(d.backdrop_path);
    if (d.images) {
      for (const key of Object.keys(d.images)) {
        d.images[key] = (d.images[key] || []).map((im) => ({ ...im, url: fullImage(im.file_path) }));
      }
    }
    return json(res, 200, d);
  } catch (e) {
    const status = /HTTP 404/.test(e.message) ? 404 : 502;
    return json(res, status, { error: `tmdb details failed: ${e.message}` });
  }
}

async function handleStreams(req, res, url) {
  const id = (url.searchParams.get('tmdb') || '').trim();
  if (!/^\d+$/.test(id)) return json(res, 400, { error: 'missing or invalid ?tmdb=<id>' });

  const type = url.searchParams.get('type') === 'tv' ? 'tv' : 'movie';
  const season = url.searchParams.get('season');
  const episode = url.searchParams.get('episode');
  if (type === 'tv' && (!season || !episode)) {
    return json(res, 400, { error: 'tv streams need ?season=<n>&episode=<n> (see /tv/<id>/seasons)' });
  }

  const providersParam = url.searchParams.get('providers');
  const names = providersParam
    ? providersParam.split(',').map((s) => s.trim()).filter(Boolean)
    : cfg.enabledProviders;
  const { providers, loadErrors } = loadProviders(names);
  if (!providers.length) return json(res, 500, { error: 'no providers available', loadErrors });

  const media = {
    id,
    type,
    seasonId: season ? parseInt(season, 10) : null,
    episodeId: episode ? parseInt(episode, 10) : null,
  };

  const { streams, errors } = await scrapeAll(providers, media, cfg.timeoutMs);
  const all = streams.map((s) => normalize(s, s._provider));
  const filters = {
    minQuality: url.searchParams.get('minQuality') || 'any',
    audioCodec: url.searchParams.get('audioCodec') || 'any',
    subtitles: url.searchParams.get('subtitles') || 'any',
    subtitleLang: url.searchParams.get('subtitleLang') || '',
  };
  const ranked = rankStreams(applyFilters(all, filters));

  return json(res, 200, {
    tmdb_id: id,
    type,
    season: media.seasonId,
    episode: media.episodeId,
    total: all.length,
    matched: ranked.length,
    streams: ranked.map((s) => ({
      provider: s.provider,
      name: s.name,
      url: s.url,
      quality: s.quality,
      pixels: s.pixels,
      audio: s.audio,
      type: s.type,
      headers: s.headers,
          subtitles: s.subtitles,
    })),
    providerErrors: errors,
    loadErrors,
  });
}

const server = http.createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
  const p = url.pathname.replace(/\/+$/, '') || '/';
  try {
    if (req.method === 'OPTIONS') {
      res.writeHead(204, {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, OPTIONS',
        'Access-Control-Allow-Headers': '*',
      });
      return res.end();
    }
    if (req.method !== 'GET') return json(res, 405, { error: 'method not allowed, use GET' });

    // web docs
    if (p === '/' || p === '/index.html') {
      const data = fs.readFileSync(SWAGGER_HTML);
      return html(res, 200, data);
    }
    if (p === '/api.json') {
      return json(res, 200, spec);
    }
    if (p === '/proxy/media') return await handleProxyMedia(req, res, url);

    // api
    if (p === '/providers') return json(res, 200, { all: listAll(), enabled: cfg.enabledProviders });
    if (p === '/search') return await handleSearch(req, res, url);
    if (p === '/trending') return await handleTrending(req, res, url);

    let m = p.match(/^\/movie\/(\d+)$/);
    if (m) return await handleDetails(req, res, 'movie', m[1]);

    m = p.match(/^\/tv\/(\d+)$/);
    if (m) return await handleDetails(req, res, 'tv', m[1]);

    m = p.match(/^\/tv\/(\d+)\/seasons$/);
    if (m) return await handleSeasons(req, res, url, m[1]);

    m = p.match(/^\/tv\/(\d+)\/seasons\/(\d+)$/);
    if (m) return await handleEpisodes(req, res, url, m[1], m[2]);

    if (p === '/streams') return await handleStreams(req, res, url);

    return json(res, 404, { error: 'not found', hint: 'see GET / (Swagger UI) or GET /api.json' });
  } catch (e) {
    if (!res.headersSent) return json(res, 500, { error: e.message });
    res.destroy();
  }
});

server.listen(PORT, HOST, () => {
  console.log(`bug.tv api: http://${HOST}:${PORT}  (docs at /)`);
  console.log(`providers: ${cfg.enabledProviders.join(', ')}`);
});
