import type { ActionDefinition, ContextEntry, FeedItemSignals, FeedSource } from "@aelis/core"

import { Context, TimeRelevance, UnknownActionError } from "@aelis/core"
import { LocationKey } from "@aelis/source-location"
import { type } from "arktype"

import type {
	ITflApi,
	StationLocation,
	TflAlertData,
	TflAlertFeedItem,
	TflAlertSeverity,
	TflLineId,
	TflSourceOptions,
} from "./types.ts"

import { TflApi, lineId } from "./tfl-api.ts"
import { TflFeedItemType } from "./types.ts"

const setLinesInput = lineId.array()

const SEVERITY_URGENCY: Record<TflAlertSeverity, number> = {
	closure: 1.0,
	"major-delays": 0.8,
	"minor-delays": 0.6,
}

const SEVERITY_TIME_RELEVANCE: Record<TflAlertSeverity, TimeRelevance> = {
	closure: TimeRelevance.Imminent,
	"major-delays": TimeRelevance.Imminent,
	"minor-delays": TimeRelevance.Upcoming,
}

/**
 * A FeedSource that provides TfL (Transport for London) service alerts.
 *
 * Depends on location source for proximity-based sorting. Produces feed items
 * for tube, overground, and Elizabeth line disruptions.
 *
 * @example
 * ```ts
 * const tflSource = new TflSource({
 *   apiKey: process.env.TFL_API_KEY!,
 *   lines: ["northern", "victoria", "jubilee"],
 * })
 *
 * const engine = new FeedEngine()
 *   .register(locationSource)
 *   .register(tflSource)
 *
 * const { items } = await engine.refresh()
 * ```
 */
export class TflSource implements FeedSource<TflAlertFeedItem> {
	static readonly DEFAULT_LINES_OF_INTEREST: readonly TflLineId[] = [
		"bakerloo",
		"central",
		"circle",
		"district",
		"hammersmith-city",
		"jubilee",
		"metropolitan",
		"northern",
		"piccadilly",
		"victoria",
		"waterloo-city",
		"lioness",
		"mildmay",
		"windrush",
		"weaver",
		"suffragette",
		"liberty",
		"elizabeth",
	]

	readonly id = "aelis.tfl"
	readonly dependencies = ["aelis.location"]

	private readonly client: ITflApi
	private lines: TflLineId[]

	constructor(options: TflSourceOptions) {
		if (!options.client && !options.apiKey) {
			throw new Error("Either client or apiKey must be provided")
		}
		this.client = options.client ?? new TflApi(options.apiKey!)
		this.lines = options.lines ?? [...TflSource.DEFAULT_LINES_OF_INTEREST]
	}

	async listActions(): Promise<Record<string, ActionDefinition>> {
		return {
			"set-lines-of-interest": {
				id: "set-lines-of-interest",
				description: "Update the set of monitored TfL lines",
				input: setLinesInput,
			},
		}
	}

	async executeAction(actionId: string, params: unknown): Promise<void> {
		switch (actionId) {
			case "set-lines-of-interest": {
				const result = setLinesInput(params)
				if (result instanceof type.errors) {
					throw new Error(result.summary)
				}
				this.setLinesOfInterest(result)
				return
			}
			default:
				throw new UnknownActionError(actionId)
		}
	}

	async fetchContext(): Promise<readonly ContextEntry[] | null> {
		return null
	}

	/**
	 * Update the set of monitored lines. Takes effect on the next fetchItems call.
	 */
	setLinesOfInterest(lines: TflLineId[]): void {
		this.lines = lines
	}

	async fetchItems(context: Context): Promise<TflAlertFeedItem[]> {
		const [statuses, stations] = await Promise.all([
			this.client.fetchLineStatuses(this.lines),
			this.client.fetchStations(),
		])

		const location = context.get(LocationKey)

		const items: TflAlertFeedItem[] = statuses.map((status) => {
			const closestStationDistance = location
				? findClosestStationDistance(status.lineId, stations, location.lat, location.lng)
				: null

			const data: TflAlertData = {
				line: status.lineId,
				lineName: status.lineName,
				severity: status.severity,
				description: status.description,
				closestStationDistance,
			}

			const signals: FeedItemSignals = {
				urgency: SEVERITY_URGENCY[status.severity],
				timeRelevance: SEVERITY_TIME_RELEVANCE[status.severity],
			}

			return {
				id: `tfl-alert-${status.lineId}-${status.severity}`,
				type: TflFeedItemType.Alert,
				timestamp: context.time,
				data,
				signals,
			}
		})

		// Sort by urgency (desc), then by proximity (asc) if location available
		items.sort((a, b) => {
			const aUrgency = a.signals?.urgency ?? 0
			const bUrgency = b.signals?.urgency ?? 0
			if (bUrgency !== aUrgency) {
				return bUrgency - aUrgency
			}
			if (a.data.closestStationDistance !== null && b.data.closestStationDistance !== null) {
				return a.data.closestStationDistance - b.data.closestStationDistance
			}
			return 0
		})

		return items
	}
}

function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
	const R = 6371 // Earth's radius in km
	const dLat = ((lat2 - lat1) * Math.PI) / 180
	const dLng = ((lng2 - lng1) * Math.PI) / 180
	const a =
		Math.sin(dLat / 2) * Math.sin(dLat / 2) +
		Math.cos((lat1 * Math.PI) / 180) *
			Math.cos((lat2 * Math.PI) / 180) *
			Math.sin(dLng / 2) *
			Math.sin(dLng / 2)
	const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
	return R * c
}

function findClosestStationDistance(
	lineId: TflLineId,
	stations: StationLocation[],
	userLat: number,
	userLng: number,
): number | null {
	const lineStations = stations.filter((s) => s.lines.includes(lineId))
	if (lineStations.length === 0) return null

	let minDistance = Infinity
	for (const station of lineStations) {
		const distance = haversineDistance(userLat, userLng, station.lat, station.lng)
		if (distance < minDistance) {
			minDistance = distance
		}
	}

	return minDistance
}
