import express from 'express';
import {resolve, dirname} from 'path';
import {fileURLToPath} from 'url';
import {cfg} 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, getAISImage} from './ais.mjs';
import {getADSBImage, getADSB, getADSBHistory, getADSBRange, initAircraftDb} from './adsb.mjs';
import {fetchIcon} from './openweather.mjs';
import {getForecast, getWeatherCondition, forecastTTL} from './forecast.mjs';
import {dailyWeather, hourlyWeather} from './openmeteo.mjs';

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

const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

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', asyncHandler(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 = {
		...condition,
		...sensors,
		...(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,
		} : {}),
		...space,
	}

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

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

app.get('/api/hourly', asyncHandler(async (req, res) => {
	const { fields } = req.query
	const coords = await getCoords()
	const now = new Date()
	now.setMinutes(0, 0, 0)
	const forecast24Limit = 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)   : forecast24Limit

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

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

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

app.get('/api/daily', asyncHandler(async (req, res) => {
	const { fields } = req.query
	const coords = await getCoords()
	const now = new Date()
	const todayStart = new Date(now)
	todayStart.setHours(0, 0, 0, 0)
	const todayEnd = new Date(todayStart)
	todayEnd.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)   : new Date(todayEnd.getTime() + 5 * 24 * 60 * 60_000)

	const [history, current, future] = await Promise.all([
		start < todayStart
			? queryDaily(start, todayStart)
			: Promise.resolve([]),
		start <= todayStart && end >= todayStart
			? queryCurrent().then(resp => [resp])
			: Promise.resolve([]),
		end > todayEnd
			? dailyWeather(coords.latitude, coords.longitude, todayEnd, end)
			: Promise.resolve([]),
	])

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

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

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

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

app.get('/api/icon/:icon', asyncHandler(async (req, res, next) => {
	const { icon } = req.params;
	if(!icon) return next(); // 404 fallback can handle missing icons
	res.contentType('image/png');
	res.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/AIS Proxy ────────────────────────────────────────────────────────────

app.get('/api/adsb', asyncHandler(async (req, res) => res.json(await getADSB())));
app.get('/api/adsb-image/:icao', asyncHandler(async (req, res) => {
	const blob = await getADSBImage(req.params.icao);
	if (!blob) return res.status(404).send('No image found');
	const buffer = Buffer.from(await blob.arrayBuffer());
	res.contentType(blob.type).send(buffer);
}));
app.get('/api/adsb/:icao', asyncHandler(async (req, res) => res.json(await getADSBHistory(req.params.icao))));
app.get('/api/ais', asyncHandler(async (req, res) => res.json(await getAIS())));
app.get('/api/ais-image/:mmsi', asyncHandler(async (req, res) => {
	const blob = await getAISImage(req.params.mmsi);
	if (!blob) return res.status(404).send('No image found');
	const buffer = Buffer.from(await blob.arrayBuffer());
	res.contentType(blob.type).send(buffer);
}));
app.get('/api/range', asyncHandler(async (req, res) => res.json(await getADSBRange())));

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

app.use('/docs', apiReference({spec: {url: '/openapi.json'}, theme: 'default'}));
app.get('/openapi.json',asyncHandler(async (req, res) => res.json(spec)));
app.get('/openapi.yaml',asyncHandler(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}`)
});
