import logging
import re
import threading
from datetime import datetime
from typing import Any

import pytz
import requests
import tornado
from tornado import httputil
from tornado.web import Application

from core.config import ALLOW_SPOTTING, ALLOW_UPSTREAM_SPOTTING, RECAPTCHA_SECRET_KEY
from core.constants import UNKNOWN_BAND
from core.prometheus_metrics_handler import api_requests_counter
from core.sig_utils import get_ref_regex_for_sig
from core.utils import infer_band_from_freq
from core.utils import safe_json_dumps
from data.spot import Spot
from providers.spot.spot_provider import SpotProvider

RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"


class APISpotHandler(tornado.web.RequestHandler):
    """API request handler for /api/v2/spot (POST)"""

    def __init__(self, application: "Application", request: httputil.HTTPServerRequest, **kwargs: Any):
        self._spots = None
        self._web_server_metrics = None
        self._spot_providers = None
        super().__init__(application, request, **kwargs)

    def initialize(self, spots, web_server_metrics, spot_providers=None):
        self._spots = spots
        self._web_server_metrics = web_server_metrics
        self._spot_providers = spot_providers or []

    def post(self):
        try:
            # Metrics
            self._web_server_metrics["last_api_access_time"] = datetime.now(pytz.UTC)
            self._web_server_metrics["api_access_counter"] += 1
            self._web_server_metrics["status"] = "OK"
            api_requests_counter.inc()

            # Reject if not allowed
            if not ALLOW_SPOTTING:
                self.set_status(401)
                self.write(safe_json_dumps("Error - this server does not allow new spots to be added via the API."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject if format not json
            if not self.request.headers.get('Content-Type', '').startswith("application/json"):
                self.set_status(415)
                self.write(safe_json_dumps("Error - request Content-Type must be application/json"))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject if request body is empty
            post_data = self.request.body
            if not post_data:
                self.set_status(422)
                self.write(safe_json_dumps("Error - request body is empty"))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Read in the request body as JSON
            json_body = tornado.escape.json_decode(post_data)

            # Extract the "spot" and "handling" sub-objects from the request body
            spot_data = json_body.get("spot", {})
            handling = json_body.get("handling", {})

            # Extract individual parameters that say how this spot should be handled by the server
            submit_upstream = handling.get("submit_upstream", False)
            upstream_provider_name = handling.get("upstream_provider", None)
            upstream_credentials = handling.get("upstream_credentials", {})
            captcha_token = handling.get("captcha_token", None)


            # Spothole v2.0 release only: deny upstream spotting. Spothole API breaking changes were in v2.0 but
            # functionality is not ready yet. TODO
            submit_upstream = False



            # Verify CAPTCHA if required
            if RECAPTCHA_SECRET_KEY:
                if not captcha_token:
                    self.set_status(422)
                    self.write(safe_json_dumps("Error - CAPTCHA token is required for spot submission."))
                    self.set_header("Cache-Control", "no-store")
                    self.set_header("Content-Type", "application/json")
                    return
                if not self._verify_recaptcha(captcha_token):
                    self.set_status(422)
                    self.write(safe_json_dumps("Error - CAPTCHA verification failed."))
                    self.set_header("Cache-Control", "no-store")
                    self.set_header("Content-Type", "application/json")
                    return

            # Convert spot field to a Spot object
            spot = Spot(**spot_data)

            # Reject if no timestamp, frequency, dx_call or de_call
            if not spot.time or not spot.dx_call or not spot.freq or not spot.de_call:
                self.set_status(422)
                self.write(
                    safe_json_dumps("Error - 'time', 'dx_call', 'freq' and 'de_call' must be provided as a minimum."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject invalid-looking callsigns
            if not re.match(r"^[A-Za-z0-9/\-]*$", spot.dx_call):
                self.set_status(422)
                self.write(safe_json_dumps("Error - '" + spot.dx_call + "' does not look like a valid callsign."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return
            if not re.match(r"^[A-Za-z0-9/\-]*$", spot.de_call):
                self.set_status(422)
                self.write(safe_json_dumps("Error - '" + spot.de_call + "' does not look like a valid callsign."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject if frequency not in a known band
            if infer_band_from_freq(spot.freq) == UNKNOWN_BAND:
                self.set_status(422)
                self.write(
                    safe_json_dumps("Error - Frequency of " + str(spot.freq / 1000.0) + "kHz is not in a known band."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject if grid formatting incorrect
            if spot.dx_grid and not re.match(
                    r"^([A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2}[A-X]{2}[0-9]{2}|[A-R]{2}[0-9]{2}[A-X]{2}|[A-R]{2}[0-9]{2})$",
                    spot.dx_grid.upper()):
                self.set_status(422)
                self.write(
                    safe_json_dumps("Error - '" + spot.dx_grid + "' does not look like a valid Maidenhead grid."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject if sig_ref format incorrect for sig
            if spot.sig and spot.sig_refs and len(spot.sig_refs) > 0 and spot.sig_refs[0].id and get_ref_regex_for_sig(
                    spot.sig) and not re.match(get_ref_regex_for_sig(spot.sig), spot.sig_refs[0].id):
                self.set_status(422)
                self.write(safe_json_dumps(
                    "Error - '" + spot.sig_refs[0].id + "' does not look like a valid reference for " + spot.sig + "."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Reject upstream submission if not permitted
            if submit_upstream and not ALLOW_UPSTREAM_SPOTTING:
                self.set_status(403)
                self.write(safe_json_dumps("Error - this server does not allow upstream spot submission."))
                self.set_header("Cache-Control", "no-store")
                self.set_header("Content-Type", "application/json")
                return

            # Validate upstream submission requirements
            if submit_upstream and upstream_provider_name:
                if not spot.sig:
                    self.set_status(422)
                    self.write(safe_json_dumps("Error - a SIG must be selected to submit upstream."))
                    self.set_header("Cache-Control", "no-store")
                    self.set_header("Content-Type", "application/json")
                    return
                if not spot.sig_refs and upstream_provider_name != "Tiles":
                    self.set_status(422)
                    self.write(safe_json_dumps("Error - a SIG reference is required to submit upstream."))
                    self.set_header("Cache-Control", "no-store")
                    self.set_header("Content-Type", "application/json")
                    return
                if not spot.dx_grid and upstream_provider_name == "Tiles":
                    self.set_status(422)
                    self.write(
                        safe_json_dumps("Error - a grid reference is required to submit upstream to Tiles on the Air."))
                    self.set_header("Cache-Control", "no-store")
                    self.set_header("Content-Type", "application/json")
                    return
                if not spot.mode and upstream_provider_name == "Tiles":
                    self.set_status(422)
                    self.write(safe_json_dumps("Error - a mode is required to submit upstream to Tiles on the Air."))
                    self.set_header("Cache-Control", "no-store")
                    self.set_header("Content-Type", "application/json")
                    return

            # Submit upstream if requested
            upstream_warning = None
            if submit_upstream and upstream_provider_name:
                provider = self._find_provider(upstream_provider_name, spot.sig)
                if provider:
                    try:
                        # Submit spot to the upstream provider
                        provider.submit_spot(spot, upstream_credentials)
                        # Trigger a re-poll after 1 second so the spot appears quickly
                        threading.Timer(1.0, provider.force_poll).start()
                    except NotImplementedError as e:
                        upstream_warning = str(e)
                    except Exception as e:
                        logging.warning("Failed to submit spot upstream to " + upstream_provider_name + ": " + str(e))
                        upstream_warning = "Spot was saved locally but upstream submission to " + upstream_provider_name + " failed: " + str(
                            e)
                else:
                    upstream_warning = "No enabled provider named '" + upstream_provider_name + "' supports upstream submission for " + (
                        spot.sig if spot.sig else "") + " spots."

            # If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a
            # duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
            # we were but it failed, we should still add it to our database anyway.
            if not submit_upstream or upstream_warning:
                spot.infer_missing()
                self._spots.set(spot.id, spot)

            if upstream_warning:
                self.write(safe_json_dumps("Warning - " + upstream_warning))
                self.set_status(201)
            else:
                self.write(safe_json_dumps("OK"))
                self.set_status(201)
            self.set_header("Cache-Control", "no-store")
            self.set_header("Content-Type", "application/json")

        except Exception as e:
            logging.error("Exception when handling client request to add spot API: %s", e, exc_info=True)
            self.write(safe_json_dumps("Error - an internal server error occurred."))
            self.set_status(500)
            self.set_header("Cache-Control", "no-store")
            self.set_header("Content-Type", "application/json")

    def _find_provider(self, provider_name, sig) -> SpotProvider | None:
        """Find an enabled provider by name that can submit spots for the given SIG."""

        for p in self._spot_providers:
            if p.enabled and p.name == provider_name and p.can_submit_spot(sig):
                return p
        return None

    @staticmethod
    def _verify_recaptcha(token):
        """Verify a Google reCAPTCHA v2 token. Returns True if valid."""

        try:
            response = requests.post(RECAPTCHA_VERIFY_URL,
                                     data={"secret": RECAPTCHA_SECRET_KEY, "response": token},
                                     timeout=(5, 10))
            return response.ok and response.json().get("success", False)
        except Exception as e:
            logging.warning("reCAPTCHA verification request failed: " + str(e))
            return False
