import csv
import logging
from datetime import datetime, timezone, timedelta
from threading import Thread, Event

import pytz
import requests

from core.constants import HTTP_HEADERS
from solarconditionsproviders.ionosonde_utils import compute_band_states
from solarconditionsproviders.solar_conditions_provider import SolarConditionsProvider

POLL_INTERVAL = 3600  # 1 hour
STATIONS_INDEX = "datafiles/didbase-stations.csv"
LGDC_URL = "https://lgdc.uml.edu/common/DIDBGetValues"
HISTORY_HOURS = 24


class GIROIonosonde(SolarConditionsProvider):
    """Solar conditions provider using ionosonde data from the GIRO Data Center.
    Queries foF2, MUF, and LUF measurements for all stations in datafiles/didbase-stations.csv.
    Can run alongside KC2GProp: GIRO supplements KC2G's foF2/MUF data with LUF readings, and
    stations from each source that the other does not cover are preserved."""

    def __init__(self, provider_config):
        super().__init__(provider_config)
        self._stations = self._load_stations()
        self._thread = None
        self._stop_event = Event()

    @staticmethod
    def _load_stations():
        stations = []
        with open(STATIONS_INDEX, newline='') as f:
            for row in csv.reader(f):
                if len(row) >= 2:
                    stations.append({"ursi": row[0].strip(), "name": row[1].strip()})
        return stations

    def setup(self, solar_conditions, solar_conditions_cache):
        """Pre-populate ionosonde_data with known station names for stations not already present,
        so the station dropdown is available before the first poll. Does not overwrite existing
        entries so KC2G cache data is preserved."""

        super().setup(solar_conditions, solar_conditions_cache)
        existing = solar_conditions.ionosonde_data or {}
        new_entries = {
            s["ursi"]: {"ursi": s["ursi"], "name": s["name"], "fof2": None, "muf": None,
                        "luf": None, "band_states": None}
            for s in self._stations if s["ursi"] not in existing
        }
        if new_entries:
            self.update_data({"ionosonde_data": {**existing, **new_entries}})

    def start(self):
        logging.info(f"Set up query of GIRO ionosonde data API every {POLL_INTERVAL} seconds.")
        self._thread = Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self):
        self._stop_event.set()

    def _run(self):
        while True:
            self._poll()
            if self._stop_event.wait(timeout=POLL_INTERVAL):
                break

    def _poll(self):
        try:
            logging.debug("Polling GIRO ionosonde data...")
            now = datetime.now(timezone.utc)
            from_time = now - timedelta(hours=HISTORY_HOURS)
            cutoff_ts = from_time.timestamp()

            # Start from the existing ionosonde_data so stations provided by other providers
            # (e.g. KC2GProp) are preserved for stations GIRO does not cover.
            ionosonde_data = dict(self._solar_conditions.ionosonde_data or {})
            updated_count = 0

            for station in self._stations:
                if self._stop_event.is_set():
                    break
                ursi = station["ursi"]
                name = station["name"]
                try:
                    fof2, muf, luf = self._fetch_station_data(ursi, from_time, now)
                    if not fof2 or not muf:
                        continue

                    # Merge GIRO's readings into any existing data for this station.
                    existing = ionosonde_data.get(ursi, {})
                    merged_fof2 = {**{float(t): v for t, v in (existing.get("fof2") or {}).items()}, **fof2}
                    merged_muf = {**{float(t): v for t, v in (existing.get("muf") or {}).items()}, **muf}
                    merged_luf = dict(luf) if luf else {}

                    merged_fof2 = {t: v for t, v in merged_fof2.items() if t >= cutoff_ts}
                    merged_muf = {t: v for t, v in merged_muf.items() if t >= cutoff_ts}
                    merged_luf = {t: v for t, v in merged_luf.items() if t >= cutoff_ts}

                    band_states = compute_band_states(merged_fof2, merged_muf, merged_luf)
                    ionosonde_data[ursi] = {
                        "ursi": ursi, "name": name,
                        "fof2": merged_fof2 or None,
                        "muf": merged_muf or None,
                        "luf": merged_luf or None,
                        "band_states": band_states,
                    }
                    updated_count += 1
                except Exception:
                    logging.warning(f"Could not fetch ionosonde data for {ursi} ({name})")

            self.update_data({"ionosonde_data": ionosonde_data})
            self.status = "OK"
            self.last_update_time = datetime.now(pytz.UTC)
            logging.debug(f"Updated ionosonde data for {updated_count} stations.")

        except Exception:
            self.status = "Error"
            logging.exception("Exception in GIRO Ionosonde data provider")
            self._stop_event.wait(timeout=1)

    def _fetch_station_data(self, ursi, from_time, to_time):
        """Fetch foF2, MUF and LUF readings for a station. Returns (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""

        from_str = from_time.strftime("%Y.%m.%d+%H:%M:%S")
        to_str = to_time.strftime("%Y.%m.%d+%H:%M:%S")
        url = f"{LGDC_URL}?ursiCode={ursi}&charName=foF2,MUFD,fmin&DMUF=3000&fromDate={from_str}&toDate={to_str}"
        response = requests.get(url, headers=HTTP_HEADERS, timeout=(5, 15))
        if response.status_code != 200:
            return None, None, None
        return self._parse_all(response.text)

    @staticmethod
    def _parse_all(text):
        """Parse web server response and return (fof2_dict, muf_dict, luf_dict) keyed by UNIX timestamp."""

        fof2_data = {}
        muf_data = {}
        luf_data = {}
        for line in text.splitlines():
            line = line.strip()
            if not line or line.startswith('#'):
                continue
            # Data rows have the following format: timestamp CS foF2 QD MUFD QD fmin QD
            parts = line.split()
            if len(parts) >= 5:
                try:
                    # Python 3.8 TZ parsing fudge
                    ts = datetime.fromisoformat(parts[0].replace('Z', '+00:00')).timestamp()
                except ValueError:
                    continue
                try:
                    fof2_data[ts] = float(parts[2])
                except ValueError:
                    pass
                try:
                    muf_data[ts] = float(parts[4])
                except ValueError:
                    pass
                if len(parts) >= 7:
                    try:
                        luf_data[ts] = float(parts[6])
                    except ValueError:
                        pass
        return fof2_data, muf_data, luf_data
