import json as builtin_json
import logging
import os
import sys
from datetime import datetime
from enum import Enum
from pathlib import Path
from urllib.error import URLError
from urllib.request import urlopen

from flask import Flask, jsonify, render_template, request
from flask_cors import CORS
from flask_json import FlaskJSON

from metobsapi import data_api, files_api
from metobsapi.util import data_responses, file_responses

LOG = logging.getLogger(__name__)

app = Flask(__name__)

# Load custom configuration file is specified
app.config.from_object("metobsapi.common_config")
if os.environ.get("METOBSAPI_SETTINGS") is not None:
    app.config.from_pyfile(os.environ.get("METOBSAPI_SETTINGS"))
app.config.from_prefixed_env(prefix="METOBSAPI")


# Load json handler and add custom enum encoder
json = FlaskJSON(app)


@json.encoder
def enum_encoder(o):
    if isinstance(o, Enum):
        return o.value
    return None


# Allow for cross-domain access to the API using CORS
CORS(app, resources=r"/api/*", allow_headers="Content-Type")


@app.route("/api/")
def index():
    """Get Main App Documentation."""
    return render_template("index.html")


@app.route("/api/files")
def files_index():
    """Get Files API Documentation."""
    return render_template(
        "files_index.html",
        archive_info=file_responses.ARCHIVE_STREAMS,
        instrument_streams=file_responses.INSTRUMENT_STREAMS,
    )


@app.route("/api/data")
def data_index():
    """Get Data API Documentation."""
    return render_template("data_index.html")


@app.errorhandler(data_api.BadHandlerFormat)
def bad_request_format(err: data_api.BadHandlerFormat):
    return render_template("400.html", format=err.bad_format), 400


@app.errorhandler(data_api.FormattedBadRequest)
def formatted_bad_request(err: data_api.FormattedBadRequest):
    return err.handler_func(None, None, err.response_info)


@app.errorhandler(404)
def page_not_found(_):
    return render_template("404.html"), 404


@app.errorhandler(500)
def internal_server(_):
    return render_template("500.html"), 500


@app.after_request
def apply_header(response):
    response.headers[data_responses.api_version_header] = data_responses.api_version
    return response


@app.route("/api/data.<fmt>", methods=["GET"])
def get_data(fmt):
    begin_time = request.args.get("begin")
    end_time = request.args.get("end")
    site = request.args.get("site")
    inst = request.args.get("inst")
    symbols = request.args.get("symbols")
    interval = request.args.get("interval", "1m")
    sep = request.args.get("sep", ",")
    order = request.args.get("order", "row")
    epoch = request.args.get("epoch")

    handler_func = data_api.get_format_handler(fmt, {"sep": sep, "order": order})
    if isinstance(handler_func, tuple):
        return handler_func
    time_parameters = data_api.parse_time_parameters(handler_func, begin_time, end_time, interval, epoch)
    return data_api.modify_data(handler_func, time_parameters, site, inst, symbols)


@app.route("/api/files.<fmt>", methods=["GET"])
def get_files(fmt):
    begin_time = request.args.get("begin")
    end_time = request.args.get("end")
    dates = request.args.get("dates")
    streams = request.args.get("streams")
    return files_api.find_stream_files(fmt, begin_time, end_time, dates, streams)


@app.route("/api/archive/info", methods=["GET"])
def get_archive_info():
    return jsonify(
        {
            "code": 200,
            "message": "",
            "sites": file_responses.ARCHIVE_INFO,
        },
    )


@app.route("/api/status", methods=["GET"])
def status_index():
    return render_template("status_index.html")


def _status_dict_to_html(response):
    items = "<br>\n".join(f"{k}: {v}" for k, v in sorted(response.items()))
    return """<html>
<body>
{}
</body>
</html>""".format(
        items,
    )


def _status_render(response, fmt):
    if fmt == "json":
        return jsonify(response)
    return _status_dict_to_html(response)


@app.route("/api/status/<site>/<inst>.<fmt>", methods=["GET"])
@app.route("/api/status/<site>/<inst>", methods=["GET"])
@app.route("/api/status/<site>.<fmt>", methods=["GET"])
@app.route("/api/status/<site>", methods=["GET"])
def get_instrument_status(site, inst=None, fmt=None):
    """See `/api/status/` for more information."""
    # defaults:
    response = {
        "name": site if inst is None else inst,
        "short_name": "",
        "long_name": "",
        "status_code": 255,
        "status_message": "",
    }
    mod_time = datetime.utcnow()

    if fmt is None:
        fmt = "html"
    if fmt not in ["html", "json"]:
        return render_template("400.html", format=fmt), 400

    site_path = Path(site)
    json_subpath = site_path / "status.json" if inst is None else site_path / inst / "status.json"

    # try to load the JSON file from the archive
    archive_root = app.config.get("ARCHIVE_ROOT")
    if archive_root.startswith("http"):
        LOG.warning("Using URL request for status JSON, not meant for operational use")
        # we aren't on a system with the archive available, fall back to URL
        # loads directly to the archive
        base_url = app.config.get("ARCHIVE_URL")
        json_url = f"{base_url}/{json_subpath}"
        try:
            # Security: We check to ensure this is an HTTP URL as a base URL.
            #   The server configuration is also the one setting what the root URL is.
            with urlopen(json_url) as json_file:
                json_str = json_file.read()
        except URLError:
            response["status_message"] = f"Could not retrieve configured status: {json_url}"
            json_str = None
    else:
        base_path = Path(archive_root)
        json_path = base_path / json_subpath
        try:
            with json_path.open("r") as json_file:
                json_str = json_file.read()
            mod_time = datetime.fromtimestamp(os.path.getmtime(json_path))
        except FileNotFoundError:
            response["status_message"] = "No status information found."
            json_str = None

    if json_str is None:
        # exit out early, we don't know what this is
        return _status_render(response, fmt)

    json_dict = builtin_json.loads(json_str)
    response["last_updated"] = mod_time.strftime("%Y-%m-%d %H:%M:%SZ")
    response.update(json_dict)
    return _status_render(response, fmt)


if __name__ == "__main__":
    app.debug = True
    bind_addr = "0.0.0.0" if len(sys.argv) <= 1 else sys.argv[0]  # noqa: S104
    app.run(bind_addr, threaded=True)