from __future__ import print_function
import json
import logging
import time
try:
    from urllib.parse import urlencode
    from urllib.request import Request, urlopen
except ImportError:
    from urllib import urlencode
    from urllib2 import Request, urlopen

from .models import Alert
from .source_result import FetchResult

LOG = logging.getLogger(__name__)


class BayouRadarClient(object):
    """Client for Bayou Radar's public weather-alert API.

    The production API currently offers a point endpoint and a bounding-box
    endpoint. Each configured monitored area therefore needs a point or bbox.
    A successful, valid response containing zero alerts is authoritative and
    must not trigger the NWS fallback.
    """

    COLLECTION_KEYS = ("features", "alerts", "items", "data", "results")

    def __init__(self, config):
        self.config = config

    def _headers(self):
        headers = {
            "User-Agent": self.config.get("user_agent") or "BayouRadarSkywarn/0.2.0",
            "Accept": "application/json, application/geo+json",
            "Cache-Control": "no-cache",
        }
        for key, value in (self.config.get("headers") or {}).items():
            if key and value is not None:
                headers[str(key)] = str(value)
        token = self.config.get("api_token")
        if token and "Authorization" not in headers:
            headers["Authorization"] = "Bearer %s" % token
        return headers

    @staticmethod
    def _number(value, label):
        try:
            return float(value)
        except (TypeError, ValueError):
            raise RuntimeError("Invalid %s coordinate: %r" % (label, value))

    def _locator(self, area):
        area = area if isinstance(area, dict) else {"code": str(area)}
        nested = area.get("bayou_radar") if isinstance(area.get("bayou_radar"), dict) else {}
        bbox = nested.get("bbox") or area.get("bbox")
        if isinstance(bbox, dict):
            return "bbox", {
                "west": self._number(bbox.get("west"), "west"),
                "south": self._number(bbox.get("south"), "south"),
                "east": self._number(bbox.get("east"), "east"),
                "north": self._number(bbox.get("north"), "north"),
                "include_zones": str(bool(self.config.get("include_zones", True))).lower(),
            }
        lat = nested.get("latitude", nested.get("lat", area.get("latitude", area.get("lat"))))
        lon = nested.get("longitude", nested.get("lon", area.get("longitude", area.get("lon"))))
        if lat is not None and lon is not None:
            return "point", {
                "lat": self._number(lat, "latitude"),
                "lon": self._number(lon, "longitude"),
            }
        default_bbox = self.config.get("default_bbox")
        if isinstance(default_bbox, dict):
            return "bbox", {
                "west": self._number(default_bbox.get("west"), "west"),
                "south": self._number(default_bbox.get("south"), "south"),
                "east": self._number(default_bbox.get("east"), "east"),
                "north": self._number(default_bbox.get("north"), "north"),
                "include_zones": str(bool(self.config.get("include_zones", True))).lower(),
            }
        raise RuntimeError("Bayou Radar location is missing; add latitude/longitude or bbox")

    def _request(self, mode, params):
        base = self.config.get("api_base", "https://backend.bayouradar.com").rstrip("/")
        endpoint = self.config.get("bbox_endpoint", "/api/alerts/active") if mode == "bbox" else self.config.get("point_endpoint", "/api/alerts")
        url = base + "/" + str(endpoint).lstrip("/") + "?" + urlencode(params)
        request = Request(url, headers=self._headers())
        response = urlopen(request, timeout=int(self.config.get("timeout_seconds", 12)))
        raw = response.read()
        if not isinstance(raw, str):
            raw = raw.decode("utf-8")
        payload = json.loads(raw)
        self._validate_payload(payload)
        return payload

    def _with_retries(self, mode, params):
        attempts = int(self.config.get("retries", 2)) + 1
        last = None
        for attempt in range(attempts):
            try:
                return self._request(mode, params)
            except Exception as exc:
                last = exc
                if attempt + 1 < attempts:
                    time.sleep(min(2 ** attempt, 5))
        raise last

    def _validate_payload(self, payload):
        if isinstance(payload, list):
            return
        if not isinstance(payload, dict):
            raise RuntimeError("Bayou Radar returned an unsupported JSON payload")
        if any(key in payload for key in self.COLLECTION_KEYS):
            for key in self.COLLECTION_KEYS:
                if key in payload and payload[key] is not None and not isinstance(payload[key], (list, dict)):
                    raise RuntimeError("Bayou Radar field %s is not a collection" % key)
            return
        if payload.get("type") == "Feature" or isinstance(payload.get("properties"), dict) or payload.get("event"):
            return
        raise RuntimeError("Bayou Radar alert response did not contain an alert collection")

    def _items(self, payload):
        if isinstance(payload, list):
            return payload
        if not isinstance(payload, dict):
            return []
        if payload.get("type") == "Feature" or isinstance(payload.get("properties"), dict) or payload.get("event"):
            return [payload]
        for key in self.COLLECTION_KEYS:
            if key not in payload:
                continue
            value = payload.get(key)
            if isinstance(value, list):
                return value
            if isinstance(value, dict):
                if value.get("type") == "FeatureCollection":
                    return value.get("features") or []
                for nested_key in self.COLLECTION_KEYS:
                    nested = value.get(nested_key)
                    if isinstance(nested, list):
                        return nested
                return list(value.values()) if value else []
            return []
        return []

    @staticmethod
    def _pick(data, *keys):
        for key in keys:
            if key in data and data.get(key) is not None:
                return data.get(key)
        return None

    def _feature(self, item):
        if not isinstance(item, dict):
            return None
        raw_props = item.get("properties") if isinstance(item.get("properties"), dict) else item
        props = {
            "id": self._pick(raw_props, "id", "identifier"),
            "event": self._pick(raw_props, "event", "title") or "Weather Alert",
            "areaDesc": self._pick(raw_props, "areaDesc", "area_desc", "area", "areas") or "",
            "headline": self._pick(raw_props, "headline", "summary") or "",
            "description": self._pick(raw_props, "description", "details") or "",
            "instruction": self._pick(raw_props, "instruction", "instructions") or "",
            "severity": self._pick(raw_props, "severity") or "Unknown",
            "urgency": self._pick(raw_props, "urgency") or "Unknown",
            "certainty": self._pick(raw_props, "certainty") or "Unknown",
            "status": self._pick(raw_props, "status") or "Actual",
            "messageType": self._pick(raw_props, "messageType", "message_type") or "Alert",
            "senderName": self._pick(raw_props, "senderName", "sender_name", "sender") or "",
            "sent": self._pick(raw_props, "sent", "issued", "published"),
            "effective": self._pick(raw_props, "effective"),
            "onset": self._pick(raw_props, "onset"),
            "expires": self._pick(raw_props, "expires", "expiration"),
            "ends": self._pick(raw_props, "ends", "end"),
            "affectedZones": self._pick(raw_props, "affectedZones", "affected_zones") or [],
            "references": self._pick(raw_props, "references") or [],
            "parameters": self._pick(raw_props, "parameters") or {},
            "geocode": self._pick(raw_props, "geocode") or {},
        }
        alert_id = item.get("id") or props.get("id") or self._pick(raw_props, "identifier")
        return {
            "type": "Feature",
            "id": alert_id,
            "properties": props,
            "geometry": item.get("geometry") if isinstance(item.get("geometry"), dict) else raw_props.get("geometry"),
        }

    @staticmethod
    def _ugc_values(feature):
        properties = feature.get("properties") or {}
        values = []
        for value in properties.get("affectedZones") or []:
            values.append(str(value))
        geocode = properties.get("geocode") or {}
        if isinstance(geocode, dict):
            for key in ("UGC", "ugc", "SAME", "same"):
                candidate = geocode.get(key)
                if isinstance(candidate, list):
                    values.extend([str(item) for item in candidate])
                elif candidate:
                    values.append(str(candidate))
        return " ".join(values).upper()

    def _consume(self, payload, area_code, result):
        for item in self._items(payload):
            feature = self._feature(item)
            if not feature:
                continue
            ugc_text = self._ugc_values(feature)
            # When UGC data is available, prevent a broad bbox response from
            # leaking alerts belonging only to neighboring jurisdictions.
            if ugc_text and area_code not in ugc_text:
                continue
            alert = Alert.from_feature(feature, [area_code])
            result.add_alert(alert)

    def fetch(self, areas):
        result = FetchResult()
        cache = {}
        for raw_area in areas:
            area = raw_area if isinstance(raw_area, dict) else {"code": str(raw_area)}
            code = str(area.get("code") or "").strip().upper()
            if not code:
                continue
            try:
                mode, params = self._locator(area)
                cache_key = (mode, tuple(sorted((str(k), str(v)) for k, v in params.items())))
                if cache_key not in cache:
                    cache[cache_key] = self._with_retries(mode, params)
                payload = cache[cache_key]
                self._validate_payload(payload)
                self._consume(payload, code, result)
                result.successful_areas.add(code)
                result.primary_successful_areas.add(code)
                result.source_by_area[code] = "bayou_radar"
            except Exception as exc:
                result.failed_areas.add(code)
                result.primary_failed_areas.add(code)
                result.primary_healthy = False
                result.source_by_area[code] = "bayou_radar_failed"
                result.errors.append("Bayou Radar %s: %s" % (code, exc))
        return result
