import { createWriteStream, existsSync, mkdirSync } from 'fs'
import { resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
import { pipeline } from 'stream/promises'
import { tzOffsetMinutes } from './config.mjs'

const DIR      = dirname(fileURLToPath(import.meta.url))
const ICON_DIR = resolve(DIR, 'public', 'icons')
const CACHE    = {}
const CACHE_MS = 15 * 60 * 1000

if (!existsSync(ICON_DIR)) mkdirSync(ICON_DIR, { recursive: true })

const WMO = {
  0:  'Clear Sky',      1:  'Mainly Clear',   2:  'Partly Cloudy',
  3:  'Overcast',       45: 'Fog',            48: 'Icy Fog',
  51: 'Light Drizzle',  53: 'Drizzle',        55: 'Heavy Drizzle',
  61: 'Light Rain',     63: 'Rain',           65: 'Heavy Rain',
  71: 'Light Snow',     73: 'Snow',           75: 'Heavy Snow',
  77: 'Snow Grains',    80: 'Light Showers',  81: 'Showers',
  82: 'Heavy Showers',  85: 'Snow Showers',   86: 'Heavy Snow Showers',
  95: 'Thunderstorm',   96: 'Thunderstorm w/ Hail', 99: 'Thunderstorm w/ Heavy Hail',
}

const OWM_ICONS = {
  0:  '01d', 1:  '01d', 2:  '02d', 3:  '04d',
  45: '50d', 48: '50d',
  51: '09d', 53: '09d', 55: '09d',
  61: '10d', 63: '10d', 65: '10d',
  71: '13d', 73: '13d', 75: '13d', 77: '13d',
  80: '09d', 81: '09d', 82: '09d',
  85: '13d', 86: '13d',
  95: '11d', 96: '11d', 99: '11d',
}

async function ensureIcon(code) {
  const owmCode  = OWM_ICONS[code] || '01d'
  const filename = `${owmCode}.png`
  const filepath = resolve(ICON_DIR, filename)
  if (!existsSync(filepath)) {
    const res = await fetch(`https://openweathermap.org/img/wn/${owmCode}@2x.png`)
    await pipeline(res.body, createWriteStream(filepath))
  }
  return `/icons/${filename}`
}

async function cachedFetch(key, url) {
  const now = Date.now()
  if (CACHE[key] && now - CACHE[key].ts < CACHE_MS) return CACHE[key].data
  const res  = await fetch(url)
  const data = await res.json()
  CACHE[key] = { ts: now, data }
  return data
}

function dewPoint(tempC, humidity) {
  const a = 17.27, b = 237.7
  const gamma = (a * tempC) / (b + tempC) + Math.log(humidity / 100)
  return Math.round((b * gamma) / (a - gamma) * 100) / 100
}

function heatIndex(tempC, humidity) {
  const T = tempC * 9 / 5 + 32
  if (T < 80) return Math.round(tempC * 100) / 100
  const HI =
      -42.379 + 2.04901523 * T + 10.14333127 * humidity
      - 0.22475541 * T * humidity - 0.00683783 * T * T
      - 0.05481717 * humidity * humidity + 0.00122874 * T * T * humidity
      + 0.00085282 * T * humidity * humidity - 0.00000199 * T * T * humidity * humidity
  return Math.round((HI - 32) * 5 / 9 * 100) / 100
}

function vaporPressureDeficit(tempC, humidity) {
  const svp = 0.6108 * Math.exp((17.27 * tempC) / (tempC + 237.3))
  return Math.round(svp * (1 - humidity / 100) * 1000) / 1000
}

function absoluteHumidity(tempC, humidity) {
  const svp = 0.6108 * Math.exp((17.27 * tempC) / (tempC + 237.3))
  return Math.round((humidity / 100 * svp * 2165) / (tempC + 273.15) * 1000) / 1000
}

function uvLabel(uv) {
  if (uv < 3)  return 'Low'
  if (uv < 6)  return 'Moderate'
  if (uv < 8)  return 'High'
  if (uv < 11) return 'Very High'
  return 'Extreme'
}

function cloudsLabel(fraction) {
  if (fraction < 0.1) return 'Clear'
  if (fraction < 0.3) return 'Mostly Clear'
  if (fraction < 0.6) return 'Partly Cloudy'
  if (fraction < 0.9) return 'Mostly Cloudy'
  return 'Overcast'
}

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

const DAILY_FIELDS = [
  'temperature_2m_max', 'temperature_2m_min',
  'precipitation_sum', 'precipitation_probability_max',
  'windspeed_10m_max', 'windgusts_10m_max', 'winddirection_10m_dominant',
  'weathercode', 'sunrise', 'sunset', 'relative_humidity_2m_mean',
  'uv_index_max', 'shortwave_radiation_sum',
].join(',')

export async function dailyWeather(lat, lon, start, end) {
  const startStr = start.toISOString().slice(0, 10)
  const endStr   = end.toISOString().slice(0, 10)
  const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` +
      `&daily=${DAILY_FIELDS}&timezone=auto&start_date=${startStr}&end_date=${endStr}`

  const data = await cachedFetch(`daily_${lat}_${lon}_${startStr}_${endStr}`, url)
  if (!data?.daily?.time) return []

  return Promise.all(data.daily.time.map(async (time, i) => {
    const code     = data.daily.weathercode[i]
    const icon     = await ensureIcon(code).catch(() => null)
    const temp     = data.daily.temperature_2m_max[i]
    const humidity = data.daily.relative_humidity_2m_mean[i]
    const uv       = data.daily.uv_index_max[i]

    // "2026-03-01" → local midnight by shifting from UTC midnight by tz offset
    const utcMidnight = new Date(`${time}T00:00:00Z`)
    const offset      = tzOffsetMinutes(utcMidnight)
    const localMidnight = new Date(utcMidnight.getTime() - offset * 60_000)

    return {
      label:                  WMO[code] || 'Unknown',
      icon,
      code,
      time:                   localMidnight,
      temperature:            temp,
      temperature_max:        temp,
      temperature_min:        data.daily.temperature_2m_min[i],
      humidity,
      humidity_abs:           absoluteHumidity(temp, humidity),
      dew_point:              dewPoint(temp, humidity),
      heat_index:             heatIndex(temp, humidity),
      vapor_pressure_deficit: vaporPressureDeficit(temp, humidity),
      precipitation:          data.daily.precipitation_sum[i],
      precipitation_chance:   data.daily.precipitation_probability_max[i],
      wind_speed:             data.daily.windspeed_10m_max[i],
      wind_gusts:             data.daily.windgusts_10m_max[i],
      wind_direction:         data.daily.winddirection_10m_dominant[i],
      uv_index:               uv,
      uv_index_label:         uvLabel(uv),
      solar_wm2:              data.daily.shortwave_radiation_sum[i],
      clouds:                 null,
      sunrise:                data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : null,
      sunset:                 data.daily.sunset[i]  ? new Date(data.daily.sunset[i])  : null,
    }
  }))
}

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

const HOURLY_FIELDS = [
  'temperature_2m', 'relative_humidity_2m', 'dewpoint_2m',
  'apparent_temperature', 'precipitation_probability', 'precipitation',
  'weathercode', 'surface_pressure', 'cloudcover',
  'windspeed_10m', 'windgusts_10m', 'winddirection_10m',
  'uv_index', 'shortwave_radiation', 'vapour_pressure_deficit',
  'visibility',
].join(',')

export async function hourlyWeather(lat, lon, start, end) {
  const startStr = start instanceof Date ? start.toISOString().slice(0, 10) : start.slice(0, 10)
  const endStr   = end instanceof Date   ? end.toISOString().slice(0, 10)   : end.slice(0, 10)
  const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}` +
      `&hourly=${HOURLY_FIELDS}&daily=sunrise,sunset&timezone=auto&start_date=${startStr}&end_date=${endStr}`

  const data = await cachedFetch(`hourly_${lat}_${lon}_${startStr}_${endStr}`, url)
  if (!data?.hourly?.time) return []

  // Open-Meteo returns local time strings e.g. "2026-03-01T06:00" (no Z)
  // sunMap keyed by date portion of those local strings
  const sunMap = {}
  if (data.daily?.time) {
    data.daily.time.forEach((date, i) => {
      sunMap[date] = {
        sunrise: data.daily.sunrise[i] ? new Date(data.daily.sunrise[i]) : null,
        sunset:  data.daily.sunset[i]  ? new Date(data.daily.sunset[i])  : null,
      }
    })
  }

  return Promise.all(data.hourly.time.map(async (time, i) => {
    const dateKey = time.slice(0, 10)             // "2026-03-01" from local string
    const sun     = sunMap[dateKey] ?? {}
    // Open-Meteo local strings have no Z — append Z to treat as UTC for ms comparison
    // This works because Open-Meteo already returns times in the location's local tz
    // and sunrise/sunset are parsed the same way, so the comparison is consistent
    const t       = new Date(time + 'Z').getTime()
    const daytime = sun.sunrise && sun.sunset
        ? t >= new Date(sun.sunrise.toISOString().replace('Z', '') + 'Z').getTime() && t < new Date(sun.sunset.toISOString().replace('Z', '') + 'Z').getTime()
        : false

    const code     = data.hourly.weathercode[i]
    const icon     = await ensureIcon(code).catch(() => null)
    const temp     = data.hourly.temperature_2m[i]
    const humidity = data.hourly.relative_humidity_2m[i]
    const uv       = data.hourly.uv_index[i]
    const clouds   = Math.round(data.hourly.cloudcover[i]) / 100

    return {
      time:                   new Date(time + 'Z'),
      daytime,
      label:                  WMO[code] || 'Unknown',
      icon,
      code,
      temperature:            temp,
      humidity,
      humidity_abs:           absoluteHumidity(temp, humidity),
      dew_point:              dewPoint(temp, humidity),
      heat_index:             heatIndex(temp, humidity),
      vapor_pressure_deficit: data.hourly.vapour_pressure_deficit[i] ?? vaporPressureDeficit(temp, humidity),
      pressure_hpa:           data.hourly.surface_pressure[i],
      precipitation:          data.hourly.precipitation[i],
      precipitation_chance:   data.hourly.precipitation_probability[i],
      wind_speed:             data.hourly.windspeed_10m[i],
      wind_gusts:             data.hourly.windgusts_10m[i],
      wind_direction:         data.hourly.winddirection_10m[i],
      uv_index:               uv,
      uv_index_label:         uvLabel(uv),
      solar_wm2:              data.hourly.shortwave_radiation[i],
      clouds_label:           cloudsLabel(clouds),
      visibility:             data.hourly.visibility[i] != null
          ? Math.round(data.hourly.visibility[i] / 1000 * 10) / 10
          : null,
    }
  }))
}
