/**
 * discover-svc — полная логика перенесена из apps/frontend/src/app/api/discover/route.ts
 * API: GET /api/v1/discover?topic=&region=&mode=
 * Ответ: { blogs: [{ title, content, url, thumbnail }] }
 */

import Fastify from 'fastify';
import cors from '@fastify/cors';
import Redis from 'ioredis';
import { searchSearxng, type SearxngSearchResult } from './searxng.js';

const PORT = parseInt(process.env.PORT ?? '3002', 10);
const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
const GHOST_URL = process.env.GHOST_URL?.trim() ?? '';
const GHOST_CONTENT_API_KEY = process.env.GHOST_CONTENT_API_KEY?.trim() ?? '';
const GEO_DEVICE_SERVICE_URL = process.env.GEO_DEVICE_SERVICE_URL ?? 'http://localhost:4002';
const PLACEHOLDER_IMAGE = 'https://placehold.co/400x225/e5e7eb/6b7280?text=Post';
const NEWS_REGION = (process.env.NEWS_REGION ?? 'auto') as string;

type Region = 'america' | 'eu' | 'russia' | 'china';
type Topic =
  | 'tech'
  | 'finance'
  | 'art'
  | 'sports'
  | 'entertainment'
  | 'gooseek';

interface GhostPost {
  title: string;
  excerpt?: string | null;
  custom_excerpt?: string | null;
  meta_description?: string | null;
  plaintext?: string | null;
  html?: string | null;
  feature_image?: string | null;
  url: string;
}

// ioredis + NodeNext: default export не распознаётся как конструктор
const redis: import('ioredis') = new (Redis as any)(REDIS_URL);
redis.on('error', () => {});

function stripHtml(html: string): string {
  return html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
}

const SOURCES_BY_REGION: Record<
  Region,
  Record<Topic, { query: string[]; links: string[] }>
> = {
  america: {
    gooseek: { query: [], links: [] },
    tech: {
      query: ['technology news', 'latest tech', 'AI', 'science and innovation'],
      links: ['techcrunch.com', 'wired.com', 'theverge.com', 'arstechnica.com'],
    },
    finance: {
      query: ['finance news', 'economy', 'stock market', 'investing'],
      links: ['bloomberg.com', 'cnbc.com', 'marketwatch.com', 'reuters.com'],
    },
    art: {
      query: ['art news', 'culture', 'modern art', 'cultural events'],
      links: ['artnews.com', 'hyperallergic.com', 'theartnewspaper.com'],
    },
    sports: {
      query: ['sports news', 'latest sports', 'cricket football tennis'],
      links: ['espn.com', 'cbssports.com', 'nfl.com', 'nba.com'],
    },
    entertainment: {
      query: ['entertainment news', 'movies', 'TV shows', 'celebrities'],
      links: ['hollywoodreporter.com', 'variety.com', 'deadline.com'],
    },
  },
  eu: {
    gooseek: { query: [], links: [] },
    tech: {
      query: ['technology news', 'tech', 'AI', 'innovation'],
      links: ['techcrunch.com', 'bbc.com', 'theguardian.com', 'reuters.com', 'euronews.com'],
    },
    finance: {
      query: ['finance news', 'economy', 'stock market', 'euro'],
      links: ['reuters.com', 'ft.com', 'bbc.com', 'euronews.com', 'politico.eu'],
    },
    art: {
      query: ['art news', 'culture', 'art', 'exhibition'],
      links: ['theguardian.com', 'bbc.com', 'dw.com', 'france24.com'],
    },
    sports: {
      query: ['sports news', 'football', 'football soccer', 'champions league'],
      links: ['bbc.com/sport', 'skysports.com', 'uefa.com', 'eurosport.com'],
    },
    entertainment: {
      query: ['entertainment news', 'films', 'music', 'culture'],
      links: ['bbc.com', 'theguardian.com', 'euronews.com', 'dw.com'],
    },
  },
  russia: {
    gooseek: { query: [], links: [] },
    tech: {
      query: ['technology news', 'tech', 'IT', 'innovation'],
      links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru', 'vedomosti.ru'],
    },
    finance: {
      query: ['finance news', 'economy', 'markets', 'ruble'],
      links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru', 'vedomosti.ru'],
    },
    art: {
      query: ['art news', 'culture', 'exhibition', 'museum'],
      links: ['tass.com', 'ria.ru', 'kommersant.ru', 'interfax.com'],
    },
    sports: {
      query: ['sports news', 'football', 'hockey', 'olympics'],
      links: ['tass.com', 'ria.ru', 'rsport.ria.ru', 'championat.com', 'sport-express.ru'],
    },
    entertainment: {
      query: ['entertainment news', 'films', 'music', 'culture'],
      links: ['tass.com', 'ria.ru', 'interfax.com', 'kommersant.ru'],
    },
  },
  china: {
    gooseek: { query: [], links: [] },
    tech: {
      query: ['technology news', 'tech', 'AI', 'innovation'],
      links: ['scmp.com', 'xinhuanet.com', 'chinadaily.com.cn', 'reuters.com'],
    },
    finance: {
      query: ['finance news', 'economy', 'China markets', 'investing'],
      links: ['scmp.com', 'chinadaily.com.cn', 'reuters.com', 'bloomberg.com'],
    },
    art: {
      query: ['art news', 'culture', 'exhibition', 'Chinese art'],
      links: ['scmp.com', 'chinadaily.com.cn', 'xinhuanet.com'],
    },
    sports: {
      query: ['sports news', 'Olympics', 'football', 'basketball'],
      links: ['scmp.com', 'chinadaily.com.cn', 'xinhuanet.com'],
    },
    entertainment: {
      query: ['entertainment news', 'films', 'music', 'culture'],
      links: ['scmp.com', 'chinadaily.com.cn', 'variety.com'],
    },
  },
};

const COUNTRY_TO_REGION: Record<string, Region> = {
  US: 'america', CA: 'america', MX: 'america',
  RU: 'russia', BY: 'russia', KZ: 'russia',
  CN: 'china', HK: 'china', TW: 'china',
  DE: 'eu', FR: 'eu', IT: 'eu', ES: 'eu', GB: 'eu', UK: 'eu',
  NL: 'eu', PL: 'eu', BE: 'eu', AT: 'eu', PT: 'eu', SE: 'eu',
  FI: 'eu', IE: 'eu', GR: 'eu', RO: 'eu', CZ: 'eu', HU: 'eu',
  BG: 'eu', HR: 'eu', SK: 'eu', SI: 'eu', LT: 'eu', LV: 'eu', EE: 'eu', DK: 'eu',
};

async function fetchGooseekPosts(): Promise<
  { title: string; content: string; url: string; thumbnail: string }[]
> {
  if (!GHOST_URL || !GHOST_CONTENT_API_KEY) {
    throw new Error(
      'Ghost не настроен. Укажите GHOST_URL и GHOST_CONTENT_API_KEY в .env'
    );
  }
  const base = GHOST_URL.replace(/\/$/, '');
  const apiUrl = `${base}/ghost/api/content/posts/?key=${GHOST_CONTENT_API_KEY}&limit=50&fields=title,excerpt,custom_excerpt,meta_description,html,feature_image,url&formats=html`;
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 15_000);
  try {
    const res = await fetch(apiUrl, {
      signal: controller.signal,
      headers: { Accept: 'application/json' },
    });
    clearTimeout(timeoutId);
    if (!res.ok) {
      if (res.status === 401) {
        throw new Error(
          'Неверный Ghost Content API Key. Получите ключ в Ghost Admin → Settings → Integrations.'
        );
      }
      throw new Error(`Ghost API: HTTP ${res.status}`);
    }
    const data = await res.json();
    const posts: GhostPost[] = data.posts ?? [];
    return posts.map((p) => {
      const excerpt =
        p.custom_excerpt?.trim() ||
        p.meta_description?.trim() ||
        p.excerpt?.trim() ||
        p.plaintext?.trim() ||
        (p.html ? stripHtml(p.html) : '');
      const content = excerpt
        ? excerpt.slice(0, 300) + (excerpt.length > 300 ? '…' : '')
        : (p.title ? `${p.title}. Читать далее →` : 'Читать далее →');
      return {
        title: p.title || 'Без названия',
        content,
        url: p.url,
        thumbnail: p.feature_image?.trim() || PLACEHOLDER_IMAGE,
      };
    });
  } catch (e) {
    clearTimeout(timeoutId);
    throw e;
  }
}

async function resolveRegion(
  regionParam: string | null,
  forwardedFor: string | null,
  userAgent: string | null
): Promise<Region> {
  if (
    NEWS_REGION !== 'auto' &&
    ['america', 'eu', 'russia', 'china'].includes(NEWS_REGION)
  ) {
    return NEWS_REGION as Region;
  }
  if (regionParam && ['america', 'eu', 'russia', 'china'].includes(regionParam)) {
    return regionParam as Region;
  }
  try {
    const geoRes = await fetch(`${GEO_DEVICE_SERVICE_URL}/api/context`, {
      headers: {
        'x-forwarded-for': forwardedFor ?? '',
        'x-real-ip': forwardedFor ?? '',
        'user-agent': userAgent ?? '',
      },
      signal: AbortSignal.timeout(5000),
    });
    const geoData = await geoRes.json();
    const cc = geoData?.geo?.countryCode;
    if (cc && COUNTRY_TO_REGION[cc]) {
      return COUNTRY_TO_REGION[cc];
    }
  } catch {
    // keep default
  }
  return 'america';
}

const app = Fastify({ logger: true });

const corsOrigin = process.env.ALLOWED_ORIGINS
  ? process.env.ALLOWED_ORIGINS.split(',').map((s) => s.trim()).filter(Boolean)
  : true;
await app.register(cors, { origin: corsOrigin });

app.get('/metrics', async (req, reply) => {
  reply.header('Content-Type', 'text/plain; charset=utf-8');
  return reply.send(
    '# HELP gooseek_up Service is up (1) or down (0)\n' +
    '# TYPE gooseek_up gauge\n' +
    'gooseek_up 1\n'
  );
});

app.get('/health', async () => ({ status: 'ok' }));
app.get('/ready', async () => {
  try {
    await redis.ping();
    return { status: 'ready', redis: 'ok' };
  } catch {
    return { status: 'degraded', redis: 'unavailable' };
  }
});

app.get<{
  Querystring: { topic?: string; region?: string; mode?: string };
}>('/api/v1/discover', async (req, reply) => {
  const topic = (req.query.topic ?? 'tech') as Topic;
  const mode = (req.query.mode ?? 'normal') as 'normal' | 'preview';

  if (topic === 'gooseek') {
    try {
      const blogs = await fetchGooseekPosts();
      return { blogs };
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      const isConnect =
        msg.includes('ECONNREFUSED') ||
        msg.includes('fetch failed') ||
        msg.includes('Failed to fetch') ||
        msg.includes('AbortError');
      return reply
        .status(isConnect ? 503 : 500)
        .send({
          message:
            !GHOST_URL || !GHOST_CONTENT_API_KEY
              ? 'Ghost не настроен. Укажите GHOST_URL и GHOST_CONTENT_API_KEY'
              : isConnect
                ? 'Не удалось подключиться к Ghost.'
                : `Ошибка загрузки постов: ${msg}`,
        });
    }
  }

  const region = await resolveRegion(
    req.query.region ?? null,
    req.headers['x-forwarded-for'] as string | null,
    req.headers['user-agent'] as string | null
  );

  const selectedTopic = SOURCES_BY_REGION[region][topic];
  const searchLang = region === 'russia' ? 'ru' : region === 'china' ? 'zh' : 'en';

  const cacheKey = `discover:${topic}:${region}:${mode}`;
  try {
    const cached = await redis.get(cacheKey);
    if (cached) {
      return JSON.parse(cached) as { blogs: unknown[] };
    }
  } catch {
    // skip cache
  }

  let data: SearxngSearchResult[] = [];

  try {
  if (mode === 'normal') {
    const seenUrls = new Set<string>();
    const searchPromises = selectedTopic.links.flatMap((link) =>
      selectedTopic.query.map((query) =>
        searchSearxng(`site:${link} ${query}`, {
          engines: ['bing news'],
          pageno: 1,
          language: searchLang,
        }).then((r) => r.results)
      )
    );
    const settled = await Promise.allSettled(searchPromises);
    const allResults = settled
      .filter(
        (r): r is PromiseFulfilledResult<SearxngSearchResult[]> =>
          r.status === 'fulfilled'
      )
      .flatMap((r) => r.value);

    data = allResults
      .flat()
      .filter((item) => {
        const url = item.url?.toLowerCase().trim();
        if (!url || seenUrls.has(url)) return false;
        seenUrls.add(url);
        return true;
      })
      .sort(() => Math.random() - 0.5);
  } else {
    const link =
      selectedTopic.links[
        Math.floor(Math.random() * selectedTopic.links.length)
      ];
    const query =
      selectedTopic.query[
        Math.floor(Math.random() * selectedTopic.query.length)
      ];
    const res = await searchSearxng(`site:${link} ${query}`, {
      engines: ['bing news'],
      pageno: 1,
      language: searchLang,
    });
    data = res.results;
  }
  } catch (err) {
    req.log.error(err);
    return reply.status(503).send({
      message:
        err instanceof Error && err.message.includes('not configured')
          ? 'SearxNG is not configured. Set SEARXNG_URL or SEARXNG_FALLBACK_URL.'
          : 'Cannot connect to SearxNG. Check configuration.',
    });
  }

  const blogs = data.map((item) => ({
    title: item.title ?? 'No title',
    content: (item.content ?? item.title ?? '').slice(0, 300),
    url: item.url ?? '',
    thumbnail: item.thumbnail ?? item.thumbnail_src ?? item.img_src ?? '',
  }));

  try {
    await redis.setex(cacheKey, 30 * 60, JSON.stringify({ blogs }));
  } catch {
    // skip cache
  }

  return { blogs };
});

try {
  await app.listen({ port: PORT, host: '0.0.0.0' });
  console.log(`discover-svc listening on :${PORT}`);
} catch (err) {
  console.error(err);
  process.exit(1);
}
