/**
 * travel-svc — Trending, Inspiration Cards, itineraries
 * docs/architecture: 01-perplexity-analogue-design.md §2.2.D
 * Redis: travel:trending, travel:inspiration, travel:itinerary:{hash}
 */

import Fastify from 'fastify';
import cors from '@fastify/cors';
import Redis from 'ioredis';
import crypto from 'node:crypto';

const PORT = parseInt(process.env.PORT ?? '3004', 10);
const REDIS_URL = process.env.REDIS_URL ?? 'redis://localhost:6379';
const OPENAI_API_KEY = process.env.OPENAI_API_KEY ?? '';
const ITINERARY_TTL = 4 * 60 * 60; // 4h

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

const STUB_TRENDING = [
  { id: 'paris', name: 'Paris', image: 'https://images.unsplash.com/photo-1502602898657-3e72660eb936?w=400', description: '' },
  { id: 'tokyo', name: 'Tokyo', image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?w=400', description: '' },
];

const STUB_INSPIRATION = [
  { title: 'Best hiking trails in Slovenia', summary: '', image: '', url: '#' },
];

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

await app.register(cors, { origin: true });

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

app.get('/api/v1/travel/trending', async (req, reply) => {
  const cached = await redis.get('travel:trending');
  if (cached) return reply.send(JSON.parse(cached));
  return reply.send({ items: STUB_TRENDING });
});

app.get('/api/v1/travel/inspiration', async (req, reply) => {
  const cached = await redis.get('travel:inspiration');
  if (cached) return reply.send(JSON.parse(cached));
  return reply.send({ items: STUB_INSPIRATION });
});

interface ItineraryDay {
  day: number;
  title: string;
  activities: string[];
  tips?: string;
}

interface ItineraryPayload {
  days: ItineraryDay[];
  summary?: string;
}

async function generateItinerary(query: string, days: number): Promise<ItineraryPayload | null> {
  if (!OPENAI_API_KEY || !query.trim()) 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: `You are a travel planner. Generate a ${days}-day itinerary in JSON format.
Return ONLY valid JSON, no markdown. Format:
{ "days": [ { "day": 1, "title": "Day 1: Arrival", "activities": ["Activity 1", "Activity 2"], "tips": "Optional tip" } ], "summary": "Brief overview" }`,
          },
          {
            role: 'user',
            content: `Create a ${days}-day itinerary for: ${query.trim()}. Include specific places, restaurants, and practical tips.`,
          },
        ],
        max_tokens: 2000,
        temperature: 0.7,
      }),
      signal: AbortSignal.timeout(60000),
    });
    if (!res.ok) return null;
    const data = (await res.json()) as { choices?: { message?: { content?: string } }[] };
    const text = data?.choices?.[0]?.message?.content?.trim();
    if (!text) return null;
    const cleaned = text.replace(/^```json?\s*/i, '').replace(/\s*```$/i, '');
    const parsed = JSON.parse(cleaned) as ItineraryPayload;
    if (!Array.isArray(parsed?.days) || parsed.days.length === 0) return null;
    return {
      days: parsed.days.map((d) => ({
        day: Number(d.day) || 0,
        title: String(d.title ?? ''),
        activities: Array.isArray(d.activities) ? d.activities.map(String) : [],
        tips: d.tips ? String(d.tips) : undefined,
      })),
      summary: parsed.summary ? String(parsed.summary) : undefined,
    };
  } catch {
    return null;
  }
}

app.post<{ Body: unknown }>('/api/v1/travel/itinerary', async (req, reply) => {
  try {
    const body = req.body as { query?: string; days?: number };
    const query = typeof body?.query === 'string' ? body.query.trim() : '';
    const days = Math.min(14, Math.max(1, Number(body?.days) || 3));
    if (!query) {
      return reply.status(400).send({ error: 'query is required' });
    }
    const hash = crypto.createHash('sha256').update(JSON.stringify({ query, days })).digest('hex').slice(0, 16);
    const cacheKey = `travel:itinerary:${hash}`;
    const cached = await redis.get(cacheKey);
    if (cached) {
      return reply.send(JSON.parse(cached));
    }
    const payload = await generateItinerary(query, days);
    if (payload) {
      await redis.setex(cacheKey, ITINERARY_TTL, JSON.stringify(payload));
      return reply.send(payload);
    }
    return reply.send({
      message: 'Itinerary generation requires OPENAI_API_KEY. Configure it in travel-svc env.',
      days: [],
    });
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to generate itinerary' });
  }
});

// Stepper state — сохранение между шагами (Поиск → Места → Маршрут → Отели → Билеты)
// Redis: travel:stepper:{sessionId} TTL 24h
const STEPPER_TTL = 86400; // 24h

app.post('/api/v1/travel/stepper/state', async (req, reply) => {
  try {
    const body = req.body as { sessionId?: string; state?: Record<string, unknown> };
    const sessionId = body?.sessionId;
    const state = body?.state;
    if (!sessionId || typeof sessionId !== 'string' || !state || typeof state !== 'object') {
      return reply.status(400).send({ error: 'sessionId and state required' });
    }
    const key = `travel:stepper:${sessionId}`;
    await redis.setex(key, STEPPER_TTL, JSON.stringify(state));
    return reply.send({ ok: true });
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to save stepper state' });
  }
});

app.get<{ Params: { sessionId: string } }>('/api/v1/travel/stepper/state/:sessionId', async (req, reply) => {
  try {
    const { sessionId } = req.params;
    const key = `travel:stepper:${sessionId}`;
    const raw = await redis.get(key);
    if (!raw) return reply.send({ state: null });
    const state = JSON.parse(raw) as Record<string, unknown>;
    return reply.send({ state });
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ error: 'Failed to load stepper state' });
  }
});

// Weather widget — proxy к Open-Meteo (перенесено из web-svc)
async function geocodeCity(city: string): Promise<{ lat: number; lng: number; name: string } | null> {
  const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(city)}&count=1`, {
    signal: AbortSignal.timeout(5000),
  });
  const data = (await res.json()) as { results?: { latitude?: number; longitude?: number; name?: string }[] };
  const result = data.results?.[0];
  if (!result?.latitude || !result?.longitude) return null;
  return { lat: result.latitude, lng: result.longitude, name: result.name ?? city };
}

app.post<{ Body: unknown }>('/api/v1/weather', async (req, reply) => {
  try {
    const body = req.body as { lat?: number; lng?: number; city?: string; measureUnit?: 'Imperial' | 'Metric' };
    let lat: number;
    let lng: number;
    let cityName: string | undefined = body.city;

    if (body.lat != null && body.lng != null) {
      lat = body.lat;
      lng = body.lng;
    } else if (body.city?.trim()) {
      const geo = await geocodeCity(body.city.trim());
      if (!geo) return reply.status(404).send({ message: 'City not found.' });
      lat = geo.lat;
      lng = geo.lng;
      cityName = geo.name;
    } else {
      return reply.status(400).send({ message: 'Invalid request. Provide lat/lng or city.' });
    }

    const unit = body.measureUnit === 'Metric' ? '' : '&temperature_unit=fahrenheit';
    const windUnit = body.measureUnit === 'Metric' ? '' : '&wind_speed_unit=mph';
    const res = await fetch(
      `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&current=weather_code,temperature_2m,is_day,relative_humidity_2m,wind_speed_10m&timezone=auto${unit}${windUnit}`,
      { signal: AbortSignal.timeout(10000) },
    );
    const data = (await res.json()) as { error?: boolean; reason?: string; current?: Record<string, number> };

    if (data.error) {
      return reply.status(500).send({ message: data.reason ?? 'Weather fetch failed' });
    }

    const code = data.current?.weather_code ?? 0;
    const isDay = data.current?.is_day === 1;
    const dayOrNight = isDay ? 'day' : 'night';
    const iconMap: Record<number, string> = {
      0: `clear-${dayOrNight}`, 2: `cloudy-1-${dayOrNight}`, 3: `cloudy-1-${dayOrNight}`,
      45: `fog-${dayOrNight}`, 48: `fog-${dayOrNight}`, 55: `rainy-1-${dayOrNight}`,
      57: `frost-${dayOrNight}`, 65: `rainy-2-${dayOrNight}`, 67: 'rain-and-sleet-mix',
      75: `snowy-2-${dayOrNight}`, 77: `snowy-1-${dayOrNight}`, 82: `rainy-3-${dayOrNight}`,
      87: `snowy-3-${dayOrNight}`, 95: `scattered-thunderstorms-${dayOrNight}`,
      99: 'severe-thunderstorm',
    };
    const condMap: Record<number, string> = {
      0: 'Clear', 1: 'Mainly Clear', 2: 'Partly Cloudy', 3: 'Cloudy', 45: 'Fog', 48: 'Fog',
      51: 'Light Drizzle', 53: 'Moderate Drizzle', 55: 'Dense Drizzle', 56: 'Light Freezing Drizzle',
      57: 'Dense Freezing Drizzle', 61: 'Slight Rain', 63: 'Moderate Rain', 65: 'Heavy Rain',
      66: 'Light Freezing Rain', 67: 'Heavy Freezing Rain', 71: 'Slight Snow Fall',
      73: 'Moderate Snow Fall', 75: 'Heavy Snow Fall', 77: 'Snow', 80: 'Slight Rain Showers',
      81: 'Moderate Rain Showers', 82: 'Heavy Rain Showers', 85: 'Slight Snow Showers',
      86: 'Moderate Snow Showers', 87: 'Heavy Snow Showers', 95: 'Thunderstorm',
      96: 'Thunderstorm with Slight Hail', 99: 'Thunderstorm with Heavy Hail',
    };
    return reply.send({
      temperature: data.current?.temperature_2m ?? 0,
      condition: condMap[code] ?? 'Clear',
      humidity: data.current?.relative_humidity_2m ?? 0,
      windSpeed: data.current?.wind_speed_10m ?? 0,
      icon: iconMap[code] ?? `clear-${dayOrNight}`,
      temperatureUnit: body.measureUnit === 'Metric' ? 'C' : 'F',
      windSpeedUnit: body.measureUnit === 'Metric' ? 'm/s' : 'mph',
      city: cityName,
    });
  } catch (err) {
    req.log.error(err);
    return reply.status(500).send({ message: 'An error has occurred.' });
  }
});

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