import type { Context, DataSource } from "@aris/core"

import {
	WeatherFeedItemType,
	type CurrentWeatherFeedItem,
	type DailyWeatherFeedItem,
	type HourlyWeatherFeedItem,
	type WeatherAlertFeedItem,
	type WeatherFeedItem,
} from "./feed-items"
import {
	ConditionCode,
	DefaultWeatherKitClient,
	Severity,
	type CurrentWeather,
	type DailyForecast,
	type HourlyForecast,
	type WeatherAlert,
	type WeatherKitClient,
	type WeatherKitCredentials,
} from "./weatherkit"

export const Units = {
	metric: "metric",
	imperial: "imperial",
} as const

export type Units = (typeof Units)[keyof typeof Units]

export interface WeatherKitDataSourceOptions {
	credentials?: WeatherKitCredentials
	client?: WeatherKitClient
	hourlyLimit?: number
	dailyLimit?: number
}

export interface WeatherKitQueryConfig {
	units?: Units
}

export class WeatherKitDataSource implements DataSource<WeatherFeedItem, WeatherKitQueryConfig> {
	private readonly DEFAULT_HOURLY_LIMIT = 12
	private readonly DEFAULT_DAILY_LIMIT = 7

	readonly type = WeatherFeedItemType.current
	private readonly client: WeatherKitClient
	private readonly hourlyLimit: number
	private readonly dailyLimit: number

	constructor(options: WeatherKitDataSourceOptions) {
		if (!options.client && !options.credentials) {
			throw new Error("Either client or credentials must be provided")
		}
		this.client = options.client ?? new DefaultWeatherKitClient(options.credentials!)
		this.hourlyLimit = options.hourlyLimit ?? this.DEFAULT_HOURLY_LIMIT
		this.dailyLimit = options.dailyLimit ?? this.DEFAULT_DAILY_LIMIT
	}

	async query(context: Context, config: WeatherKitQueryConfig = {}): Promise<WeatherFeedItem[]> {
		if (!context.location) {
			return []
		}

		const units = config.units ?? Units.metric
		const timestamp = context.time

		const response = await this.client.fetch({
			lat: context.location.lat,
			lng: context.location.lng,
		})

		const items: WeatherFeedItem[] = []

		if (response.currentWeather) {
			items.push(createCurrentWeatherFeedItem(response.currentWeather, timestamp, units))
		}

		if (response.forecastHourly?.hours) {
			const hours = response.forecastHourly.hours.slice(0, this.hourlyLimit)
			for (let i = 0; i < hours.length; i++) {
				const hour = hours[i]
				if (hour) {
					items.push(createHourlyWeatherFeedItem(hour, i, timestamp, units))
				}
			}
		}

		if (response.forecastDaily?.days) {
			const days = response.forecastDaily.days.slice(0, this.dailyLimit)
			for (let i = 0; i < days.length; i++) {
				const day = days[i]
				if (day) {
					items.push(createDailyWeatherFeedItem(day, i, timestamp, units))
				}
			}
		}

		if (response.weatherAlerts?.alerts) {
			for (const alert of response.weatherAlerts.alerts) {
				items.push(createWeatherAlertFeedItem(alert, timestamp))
			}
		}

		return items
	}
}

const BASE_PRIORITY = {
	current: 0.5,
	hourly: 0.3,
	daily: 0.2,
	alert: 0.7,
} as const

const SEVERE_CONDITIONS = new Set<ConditionCode>([
	ConditionCode.SevereThunderstorm,
	ConditionCode.Hurricane,
	ConditionCode.Tornado,
	ConditionCode.TropicalStorm,
	ConditionCode.Blizzard,
	ConditionCode.FreezingRain,
	ConditionCode.Hail,
	ConditionCode.Frigid,
	ConditionCode.Hot,
])

const MODERATE_CONDITIONS = new Set<ConditionCode>([
	ConditionCode.Thunderstorm,
	ConditionCode.IsolatedThunderstorms,
	ConditionCode.ScatteredThunderstorms,
	ConditionCode.HeavyRain,
	ConditionCode.HeavySnow,
	ConditionCode.FreezingDrizzle,
	ConditionCode.BlowingSnow,
])

function adjustPriorityForCondition(basePriority: number, conditionCode: ConditionCode): number {
	if (SEVERE_CONDITIONS.has(conditionCode)) {
		return Math.min(1, basePriority + 0.3)
	}
	if (MODERATE_CONDITIONS.has(conditionCode)) {
		return Math.min(1, basePriority + 0.15)
	}
	return basePriority
}

function adjustPriorityForAlertSeverity(severity: Severity): number {
	switch (severity) {
		case Severity.Extreme:
			return 1
		case Severity.Severe:
			return 0.9
		case Severity.Moderate:
			return 0.75
		case Severity.Minor:
			return BASE_PRIORITY.alert
	}
}

function convertTemperature(celsius: number, units: Units): number {
	if (units === Units.imperial) {
		return (celsius * 9) / 5 + 32
	}
	return celsius
}

function convertSpeed(kmh: number, units: Units): number {
	if (units === Units.imperial) {
		return kmh * 0.621371
	}
	return kmh
}

function convertDistance(km: number, units: Units): number {
	if (units === Units.imperial) {
		return km * 0.621371
	}
	return km
}

function convertPrecipitation(mm: number, units: Units): number {
	if (units === Units.imperial) {
		return mm * 0.0393701
	}
	return mm
}

function convertPressure(mb: number, units: Units): number {
	if (units === Units.imperial) {
		return mb * 0.02953
	}
	return mb
}

function createCurrentWeatherFeedItem(
	current: CurrentWeather,
	timestamp: Date,
	units: Units,
): CurrentWeatherFeedItem {
	const priority = adjustPriorityForCondition(BASE_PRIORITY.current, current.conditionCode)

	return {
		id: `weather-current-${timestamp.getTime()}`,
		type: WeatherFeedItemType.current,
		priority,
		timestamp,
		data: {
			conditionCode: current.conditionCode,
			daylight: current.daylight,
			humidity: current.humidity,
			precipitationIntensity: convertPrecipitation(current.precipitationIntensity, units),
			pressure: convertPressure(current.pressure, units),
			pressureTrend: current.pressureTrend,
			temperature: convertTemperature(current.temperature, units),
			temperatureApparent: convertTemperature(current.temperatureApparent, units),
			uvIndex: current.uvIndex,
			visibility: convertDistance(current.visibility, units),
			windDirection: current.windDirection,
			windGust: convertSpeed(current.windGust, units),
			windSpeed: convertSpeed(current.windSpeed, units),
		},
	}
}

function createHourlyWeatherFeedItem(
	hourly: HourlyForecast,
	index: number,
	timestamp: Date,
	units: Units,
): HourlyWeatherFeedItem {
	const priority = adjustPriorityForCondition(BASE_PRIORITY.hourly, hourly.conditionCode)

	return {
		id: `weather-hourly-${timestamp.getTime()}-${index}`,
		type: WeatherFeedItemType.hourly,
		priority,
		timestamp,
		data: {
			forecastTime: new Date(hourly.forecastStart),
			conditionCode: hourly.conditionCode,
			daylight: hourly.daylight,
			humidity: hourly.humidity,
			precipitationAmount: convertPrecipitation(hourly.precipitationAmount, units),
			precipitationChance: hourly.precipitationChance,
			precipitationType: hourly.precipitationType,
			temperature: convertTemperature(hourly.temperature, units),
			temperatureApparent: convertTemperature(hourly.temperatureApparent, units),
			uvIndex: hourly.uvIndex,
			windDirection: hourly.windDirection,
			windGust: convertSpeed(hourly.windGust, units),
			windSpeed: convertSpeed(hourly.windSpeed, units),
		},
	}
}

function createDailyWeatherFeedItem(
	daily: DailyForecast,
	index: number,
	timestamp: Date,
	units: Units,
): DailyWeatherFeedItem {
	const priority = adjustPriorityForCondition(BASE_PRIORITY.daily, daily.conditionCode)

	return {
		id: `weather-daily-${timestamp.getTime()}-${index}`,
		type: WeatherFeedItemType.daily,
		priority,
		timestamp,
		data: {
			forecastDate: new Date(daily.forecastStart),
			conditionCode: daily.conditionCode,
			maxUvIndex: daily.maxUvIndex,
			precipitationAmount: convertPrecipitation(daily.precipitationAmount, units),
			precipitationChance: daily.precipitationChance,
			precipitationType: daily.precipitationType,
			snowfallAmount: convertPrecipitation(daily.snowfallAmount, units),
			sunrise: new Date(daily.sunrise),
			sunset: new Date(daily.sunset),
			temperatureMax: convertTemperature(daily.temperatureMax, units),
			temperatureMin: convertTemperature(daily.temperatureMin, units),
		},
	}
}

function createWeatherAlertFeedItem(alert: WeatherAlert, timestamp: Date): WeatherAlertFeedItem {
	const priority = adjustPriorityForAlertSeverity(alert.severity)

	return {
		id: `weather-alert-${alert.id}`,
		type: WeatherFeedItemType.alert,
		priority,
		timestamp,
		data: {
			alertId: alert.id,
			areaName: alert.areaName,
			certainty: alert.certainty,
			description: alert.description,
			detailsUrl: alert.detailsUrl,
			effectiveTime: new Date(alert.effectiveTime),
			expireTime: new Date(alert.expireTime),
			severity: alert.severity,
			source: alert.source,
			urgency: alert.urgency,
		},
	}
}
