/**
 * finance-svc — Market data, heatmap, news по тикерам
 * docs/architecture: 01-perplexity-analogue-design.md §2.2.C
 * Redis: finance:summary, finance:heatmap, finance:news:{ticker}
 */

import Fastify from 'fastify';
import cors from '@fastify/cors';
import Redis from 'ioredis';

const PORT = parseInt(process.env.PORT ?? '3003', 10);
const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
const FMP_API_KEY = process.env.FMP_API_KEY ?? '';

async function fetchWithRetry(url: string, opts: RequestInit = {}, maxAttempts = 3): Promise<Response> {
  let lastErr: Error | null = null;
  for (let i = 0; i < maxAttempts; i++) {
    try {
      const res = await fetch(url, { ...opts, signal: AbortSignal.timeout(10000) });
      if (res.ok || res.status < 500) return res;
      lastErr = new Error(`FMP HTTP ${res.status}`);
    } catch (err) {
      lastErr = err instanceof Error ? err : new Error(String(err));
    }
    if (i < maxAttempts - 1) await new Promise((r) => setTimeout(r, 500 * (i + 1)));
  }
  throw lastErr;
}

// @ts-expect-error — ioredis + NodeNext ESM constructability
const redis = new Redis(REDIS_URL);

const STUB_SUMMARY = {
  indices: [
    { symbol: 'SPX', name: 'S&P 500', price: 0, change: 0 },
    { symbol: 'NDX', name: 'NASDAQ', price: 0, change: 0 },
    { symbol: 'VIX', name: 'VIX', price: 0, change: 0 },
  ],
  updated_at: Date.now(),
};

const STUB_HEATMAP = { sectors: [], updated_at: Date.now() };
const STUB_NEWS = { items: [], updated_at: Date.now() };

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('/health', async () => ({ status: 'ok' }));

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('/ready', async () => {
  try {
    await redis.ping();
    return { status: 'ready' };
  } catch {
    return { status: 'degraded' };
  }
});

app.get('/api/v1/finance/summary', async (req, reply) => {
  const cached = await redis.get('finance:summary');
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/quote/^GSPC,^IXIC,^VIX?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const payload = {
          indices: (Array.isArray(data) ? data : []).map((q: { symbol?: string; name?: string; price?: number; changesPercentage?: number }) => ({
            symbol: q.symbol,
            name: q.name,
            price: q.price ?? 0,
            change: q.changesPercentage ?? 0,
          })),
          updated_at: Date.now(),
        };
        await redis.setex('finance:summary', 5 * 60, JSON.stringify(payload));
        return payload;
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_SUMMARY);
});

app.get('/api/v1/finance/heatmap', async (req, reply) => {
  const cached = await redis.get('finance:heatmap');
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/sector-performance?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const sectors = (Array.isArray(data) ? data : []).map(
          (s: { sector?: string; changesPercentage?: number }) => ({
            name: s.sector ?? '',
            change: Number(s.changesPercentage) ?? 0,
          })
        );
        const payload = { sectors, updated_at: Date.now() };
        await redis.setex('finance:heatmap', 5 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_HEATMAP);
});

const STUB_MOVERS = { items: [], updated_at: Date.now() };
app.get('/api/v1/finance/gainers', async (req, reply) => {
  const cached = await redis.get('finance:gainers');
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/stock_market/gainers?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const items = (Array.isArray(data) ? data : []).slice(0, 15);
        const payload = { items, updated_at: Date.now() };
        await redis.setex('finance:gainers', 2 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_MOVERS);
});
app.get('/api/v1/finance/losers', async (req, reply) => {
  const cached = await redis.get('finance:losers');
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/stock_market/losers?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const items = (Array.isArray(data) ? data : []).slice(0, 15);
        const payload = { items, updated_at: Date.now() };
        await redis.setex('finance:losers', 2 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_MOVERS);
});
app.get('/api/v1/finance/crypto', async (req, reply) => {
  const cached = await redis.get('finance:crypto');
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const symbols = 'BTCUSD,ETHUSD,BNBUSD,SOLUSD,XRPUSD,ADAUSD,DOGEUSD,AVAXUSD,MATICUSD,LINKUSD';
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/quote/${symbols}?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const items = (Array.isArray(data) ? data : []).slice(0, 20);
        const payload = { items, updated_at: Date.now() };
        await redis.setex('finance:crypto', 2 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_MOVERS);
});

app.get<{ Params: { ticker: string } }>('/api/v1/finance/news/:ticker', async (req, reply) => {
  const { ticker } = req.params;
  const key = `finance:news:${ticker.toUpperCase()}`;
  const cached = await redis.get(key);
  if (cached) return reply.send(JSON.parse(cached));
  return reply.send(STUB_NEWS);
});

const STUB_QUOTE = { symbol: '', name: '', price: 0, change: 0, updated_at: Date.now() };
app.get<{ Params: { ticker: string } }>('/api/v1/finance/quote/:ticker', async (req, reply) => {
  const ticker = req.params.ticker.toUpperCase();
  const key = `finance:quote:${ticker}`;
  const cached = await redis.get(key);
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/quote/${ticker}?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const q = Array.isArray(data) && data[0] ? data[0] : null;
        const payload = q
          ? {
              symbol: q.symbol ?? ticker,
              name: q.name ?? ticker,
              price: Number(q.price) ?? 0,
              change: Number(q.changesPercentage) ?? 0,
              high: Number(q.dayHigh) ?? 0,
              low: Number(q.dayLow) ?? 0,
              updated_at: Date.now(),
            }
          : { ...STUB_QUOTE, symbol: ticker };
        await redis.setex(key, 2 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send({ ...STUB_QUOTE, symbol: ticker });
});

const STUB_ANALYST = { ratings: [], symbol: '', updated_at: Date.now() };
app.get<{ Params: { ticker: string } }>('/api/v1/finance/analyst-ratings/:ticker', async (req, reply) => {
  const ticker = req.params.ticker.toUpperCase();
  const key = `finance:analyst:${ticker}`;
  const cached = await redis.get(key);
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/analyst-stock-recommendations/${ticker}?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const payload = { ratings: Array.isArray(data) ? data : [], symbol: ticker, updated_at: Date.now() };
        await redis.setex(key, 24 * 60 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_ANALYST);
});

const STUB_SEC = { filings: [], symbol: '', updated_at: Date.now() };
app.get<{ Params: { ticker: string } }>('/api/v1/finance/sec-filings/:ticker', async (req, reply) => {
  const ticker = req.params.ticker.toUpperCase();
  const key = `finance:sec:${ticker}`;
  const cached = await redis.get(key);
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/sec_filings/${ticker}?apikey=${FMP_API_KEY}&limit=20`
      );
      if (res.ok) {
        const data = await res.json();
        const payload = { filings: Array.isArray(data) ? data : [], symbol: ticker, updated_at: Date.now() };
        await redis.setex(key, 60 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_SEC);
});

const STUB_ETF = { holdings: [], symbol: '', updated_at: Date.now() };
app.get<{ Params: { symbol: string } }>('/api/v1/finance/etf-holdings/:symbol', async (req, reply) => {
  const symbol = req.params.symbol.toUpperCase();
  const key = `finance:etf:${symbol}`;
  const cached = await redis.get(key);
  if (cached) return reply.send(JSON.parse(cached));
  if (FMP_API_KEY) {
    try {
      const res = await fetchWithRetry(
        `https://financialmodelingprep.com/api/v3/etf-holder/${symbol}?apikey=${FMP_API_KEY}`
      );
      if (res.ok) {
        const data = await res.json();
        const payload = { holdings: Array.isArray(data) ? data : [], symbol, updated_at: Date.now() };
        await redis.setex(key, 60 * 60, JSON.stringify(payload));
        return reply.send(payload);
      }
    } catch (err) {
      req.log.warn(err);
    }
  }
  return reply.send(STUB_ETF);
});

app.get<{ Params: { id: string } }>('/api/v1/finance/predictions/:id', async (req, reply) => {
  const { id } = req.params;
  return reply.send({
    id,
    question: 'Prediction market',
    outcomes: [],
    volume: 0,
    endDate: null,
    updated_at: Date.now(),
    _stub: true,
  });
});

/**
 * Heatmap hover: LLM-синтез причины движения цены
 * docs/architecture: 01-perplexity-analogue-design.md §2.2.C
 */
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? '';

async function synthesizePriceContext(ticker: string, quote: { change: number }, newsHeadlines: string[]): Promise<string | null> {
  if (!OPENAI_API_KEY) return null;
  try {
    const res = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${OPENAI_API_KEY}`,
      },
      body: JSON.stringify({
        model: 'gpt-4o-mini',
        messages: [
          {
            role: 'system',
            content: 'Summarize in 1-2 short sentences why this stock/sector may have moved. Be concise.',
          },
          {
            role: 'user',
            content: `Ticker: ${ticker}, today's change: ${quote.change}%. Recent headlines: ${newsHeadlines.slice(0, 5).join('; ') || 'none'}.`,
          },
        ],
        max_tokens: 100,
        temperature: 0.3,
      }),
      signal: AbortSignal.timeout(5000),
    });
    if (!res.ok) return null;
    const data = (await res.json()) as { choices?: { message?: { content?: string } }[] };
    const text = data.choices?.[0]?.message?.content?.trim();
    return text || null;
  } catch {
    return null;
  }
}

app.get<{ Params: { ticker: string } }>('/api/v1/finance/price-context/:ticker', async (req, reply) => {
  const ticker = req.params.ticker.toUpperCase();
  const key = `finance:pricecontext:${ticker}`;
  const cached = await redis.get(key);
  if (cached) return reply.send(JSON.parse(cached));

  let quote = { change: 0 };
  const newsHeadlines: string[] = [];

  if (FMP_API_KEY) {
    try {
      const [quoteRes, newsRes] = await Promise.all([
        fetchWithRetry(
          `https://financialmodelingprep.com/api/v3/quote/${ticker}?apikey=${FMP_API_KEY}`
        ),
        fetchWithRetry(
          `https://financialmodelingprep.com/api/v3/stock_news?tickers=${ticker}&limit=5&apikey=${FMP_API_KEY}`
        ),
      ]);
      if (quoteRes.ok) {
        const qData = await quoteRes.json();
        const q = Array.isArray(qData) && qData[0] ? qData[0] : null;
        quote = { change: Number(q?.changesPercentage) ?? 0 };
      }
      if (newsRes.ok) {
        const nData = await newsRes.json();
        const items = Array.isArray(nData) ? nData : [];
        newsHeadlines.push(...items.slice(0, 5).map((n: { title?: string }) => String(n.title ?? '')));
      }
    } catch (err) {
      req.log.warn(err);
    }
  }

  let summary: string | null = null;
  if (OPENAI_API_KEY && (quote.change !== 0 || newsHeadlines.length > 0)) {
    summary = await synthesizePriceContext(ticker, quote, newsHeadlines);
  }

  const payload = {
    ticker,
    quote: { change: quote.change },
    news: newsHeadlines.filter(Boolean),
    summary,
    updated_at: Date.now(),
  };
  await redis.setex(key, 5 * 60, JSON.stringify(payload));
  return reply.send(payload);
});

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