import express from 'express';
import {resolve, dirname} from 'path';
import {fileURLToPath} from 'url';
import {cfg, localDateStr} from './config.mjs';
import {queryCurrent, queryHourly, queryDaily, getCoords} from './sensors.mjs';
import {getCelestialCurrent, getCelestialForecast} from './celestial.mjs';
import {apiReference} from '@scalar/express-api-reference';
import {spec} from './spec.mjs';
import {existsSync} from 'fs';
import {getAIS} from './ais.mjs';
import {getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
import {fetchIcon} from './openweather.mjs';
import {lastForecast, getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';

// ── Uncaught error handlers ───────────────────────────────────────────────────

process.on('unhandledRejection', (reason, promise) => {
	console.error('[unhandledRejection]', promise, '\nReason:', reason)
})

process.on('uncaughtException', (err) => {
	console.error('[uncaughtException]', err)
})

const app = express();
const DIR = dirname(fileURLToPath(import.meta.url));
const CLIENT_DIST = resolve(DIR, '../public');

app.use(express.json());
app.use((req, res, next) => {
	res.setHeader('Access-Control-Allow-Origin', '*');
	next();
});
app.use(express.static(CLIENT_DIST));

function filterFields(obj, fields) {
	if(!fields) return obj;
	const keys = new Set(fields.split(',').map(f => f.trim()));
	return Object.fromEntries(Object.entries(obj).filter(([k]) => keys.has(k)));
}

function filterArr(arr, fields) {
	if(!fields) return arr;
	return arr.map(row => filterFields(row, fields));
}

// ── Current ───────────────────────────────────────────────────────────────────

app.get('/api/current', async (req, res) => {
	const { fields } = req.query
	const [sensors, coords] = await Promise.all([queryCurrent(), getCoords()])
	const space     = getCelestialCurrent(coords.latitude, coords.longitude)
	const condition = getWeatherCondition(Object.assign(sensors, space))

	const forecast = await getForecast();
	const merged = {
		...(forecast?.summary ? {
			precipitation_chance: forecast.summary.precipitation_chance,
			temperature_max:      forecast.summary.temperature_max,
			temperature_min:      forecast.summary.temperature_min,
			uv_index_max:         forecast.summary.uv_index,
		} : {}),
		...condition,
		...sensors,
		...space,
	}

	res.json(filterFields(merged, fields))
})

// ── Hourly ────────────────────────────────────────────────────────────────────

app.get('/api/hourly', async (req, res) => {
	const { fields } = req.query
	const now = new Date()
	now.setMinutes(0, 0, 0)
	const forecastLimit = new Date(now.getTime() + 24 * 60 * 60_000)

	const start = req.query.start ? new Date(req.query.start) : now
	const end   = req.query.end   ? new Date(req.query.end)   : forecastLimit

	const coords = await getCoords()

	const [history, forecast24, forecast3rdParty] = await Promise.all([
		start.getTime() < now.getTime() ? queryHourly(start, now) : Promise.resolve([]),
		end.getTime() > now.getTime() ? getForecast().then(f => f.slots.filter(h => new Date(h.time) <= end)) : Promise.resolve([]),
		end.getTime() > forecastLimit.getTime() ? hourlyWeather(coords.latitude, coords.longitude, forecastLimit, end) : Promise.resolve([]),
	])

	const hourly = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...forecast24, ...forecast3rdParty])
	res.json(filterArr(hourly, fields))
})

// ── Daily ─────────────────────────────────────────────────────────────────────

app.get('/api/daily', async (req, res) => {
	const { fields } = req.query
	const now  = new Date()
	const todayStart = new Date(now)
	todayStart.setHours(0, 0, 0, 0)
	const next = new Date(todayStart)
	next.setDate(todayStart.getDate() + 1)

	const start = req.query.start ? new Date(req.query.start) : todayStart
	const end   = req.query.end   ? new Date(req.query.end)   : next

	const coords     = await getCoords()
	const todayStr   = localDateStr(now)
	const usePhysics = lastForecast.summary && start <= now && end > now

	const [sensorResult] = await Promise.allSettled([start < now ? queryDaily(start, now) : Promise.resolve([])])
	const history = sensorResult.status === 'fulfilled' ? sensorResult.value : []

	const meteoFrom = new Date(Math.max(next.getTime(), start.getTime()))
	const [meteoResult] = await Promise.allSettled([end > next ? dailyWeather(coords.latitude, coords.longitude, meteoFrom, end) : Promise.resolve([])])
	const meteo = meteoResult.status === 'fulfilled' ? meteoResult.value : []

	const todaySlot  = usePhysics ? [lastForecast.summary] : []
	const meteoClean = meteo.filter(r => localDateStr(new Date(r.time)) !== todayStr)

	const daily = getCelestialForecast(coords.latitude, coords.longitude, [...history, ...todaySlot, ...meteoClean])
	res.json(filterArr(daily, fields))
})

// ── Position ──────────────────────────────────────────────────────────────────

app.get('/api/position', async (req, res) => {
	const {fields} = req.query;
	const data = await getCoords();
	res.json(filterFields(data, fields));
});

// ── Icon ──────────────────────────────────────────────────────────────────────

app.get('/api/icon/:icon', async (req, resp, next) => {
	const { icon } = req.params;
	if(!icon) return next(); // 404 fallback can handle missing icons
	resp.contentType('image/png');
	resp.sendFile(await fetchIcon(icon));
});

// ── Space ─────────────────────────────────────────────────────────────────────

// app.get('/api/aurora', async (req, res) => res.json(await Aurora.get()));
// app.get('/api/space', async (req, res) => {
// 	const {fields} = req.query;
// 	res.json(filterFields(await getSpaceWeather(), fields));
// });

// ── ADSB Proxy ────────────────────────────────────────────────────────────────

app.get('/api/adsb', async (req, res) => res.json(await getADSB()));
app.get('/api/adsb/:icao', async (req, res) => res.json(await getADSBHistory(req.params.icao)));
app.get('/api/ais', async (req, res) => res.json(await getAIS()));
app.get('/api/range', async (req, res) => res.json(await getADSBRange()));

// ── DOCS ──────────────────────────────────────────────────────────────────────

app.use('/docs', apiReference({spec: {url: '/openapi.json'}, theme: 'default'}));
app.get('/openapi.json', (req, res) => res.json(spec));
app.get('/openapi.yaml', async (req, res) => {
	const {stringify} = await import('yaml');
	res.setHeader('Content-Type', 'text/yaml');
	res.send(stringify(spec));
});

// ── Website ───────────────────────────────────────────────────────────────────

app.get('*', (req, res) => {
	const index = resolve(CLIENT_DIST, 'index.html');
	if(existsSync(index)) res.sendFile(index);
	else res.status(404).send('Client not built yet — run npm run build in /client');
});

// ── Errors ────────────────────────────────────────────────────────────────────

app.use((req, res, next) => {
	res.status(404).json({ error: `Not found: ${req.method} ${req.path}` })
})

app.use((err, req, res, next) => {
	console.error(`[ERROR] ${req.method} ${req.path}\n${err.stack}`)
	res.status(500).json({ error: err.message, stack: err.stack })
})

// ── Start ─────────────────────────────────────────────────────────────────────

const c = cfg();

setTimeout(getForecast, 1)
setInterval(getForecast, forecastTTL)
await initAircraftDb();

app.listen(c.PORT, () => {
	console.log(`⛅  Weather API — http://localhost:${c.PORT}`)
});
