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

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

from core.config import ALLOW_SPOTTING
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 safe_json_dumps, infer_band_from_freq
from data.spot import Spot


class V1APISpotHandler(tornado.web.RequestHandler):
    """API request handler for /api/v1/spot (POST). Included in early Spothole v2 for backwards compatibility."""

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

    def initialize(self, spots, web_server_metrics):
        self._spots = spots
        self._web_server_metrics = web_server_metrics

    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 then convert to a Spot object
            json_spot = tornado.escape.json_decode(post_data)
            spot = Spot(**json_spot)

            # 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

            # infer missing data, and add it to our database.
            spot.source = "API"
            spot.infer_missing()
            self._spots.set(spot.id, spot)

            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")
