#!/usr/bin/env python3

#  Copyright 2020 Matteo Pergolesi <matpergo [at] gmail [dot] com>
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
import copy
import json
from typing import Any, Dict, List, Tuple, Union

import yaml
from openapi_spec_validator import validate_spec


def _get_type_ex(val: Any, example: bool = True) -> Dict:
    ex = val
    if val is None:
        # If no value is provided, assume string
        t = "string"
        ex = ""
    elif isinstance(val, str):
        t = "string"
    elif isinstance(val, int):
        t = "integer"
    elif isinstance(val, float):
        t = "number"
    elif isinstance(val, bool):
        t = "boolean"
    else:
        t = ""
        print(f"Unknown type: {type(val)}, value: {val}")

    if example:
        return {"type": t, "example": ex}
    else:
        return {"type": t}


def _gen_schema(data: Union[Dict, List], example: bool = True) -> Dict:
    if isinstance(data, dict):
        schema = {"type": "object", "properties": {}}
        for key, val in data.items():
            schema["properties"][key] = _gen_schema(val, example)
    elif isinstance(data, list):
        schema = {"type": "array", "items": {}}
        if data:
            schema["items"] = _gen_schema(data[0], example)
    else:
        schema = _get_type_ex(data, example)
    return schema


def _load_json_yaml(s: str):
    try:
        return json.loads(s)
    except json.JSONDecodeError:
        try:
            return yaml.safe_load(s)
        except yaml.YAMLError:
            raise ValueError("Not a valid JSON or YAML")


def build_openapi(
    method: str,
    path: str,
    resp_code: str,
    parameters: List[Tuple[str, str]] = None,
    request: str = None,
    response: str = None,
    media_type: str = "application/json",
    example: bool = True,
    reference: bool = False,
    title: str = "Generated by InducOapi",
    version: str = "v1",
) -> Dict:
    """
    Builds a dict containing a valid OpenAPI description document.
    :param method: The HTTP request method to be generated
    :param path: The resource path to be generated
    :param resp_code: The HTTP response code to be generated
    :param parameters: The operation parameters as a list of tuples: ("name", "in")
    :param request: A JSON/YAML string containing a request example
    :param response: A JSON/YAML string containing a request example
    :param media_type: The desired media type to be used for request/response
    :param example: If True, generates 'example:' fields in the OpenAPI
    :param reference: If True, generates response content schemas in 'components'
    :param title: The title of the OpenAPI document (in info)
    :param version: The version of the OpenAPI document (in info)
    :return: A python dict containing a valid OpenAPI description document
    :raises: openapi3.SpecError if the generated OpenAPI is not valid
    :raises: ValueError if the data in request/response is not a valid JSON/YAML
    """
    oapi = {
        "openapi": "3.0.0",
        "info": {
            "title": title,
            "version": version,
        },
        "paths": {
            path: {
                method.lower(): {
                    "requestBody": None,
                    "responses": {
                        resp_code: {
                            "description": "",
                        }
                    },
                }
            }
        },
    }

    if parameters:
        param_list = []
        for name, location in parameters:
            required = True if location == "path" else False
            param_list.append(
                {
                    "name": name,
                    "in": location,
                    "required": required,
                    "description": "",
                    "schema": {},
                }
            )
        oapi["paths"][path][method.lower()]["parameters"] = param_list

    if request:
        try:
            data = _load_json_yaml(request)
        except ValueError as e:
            raise ValueError(f"Cannot load request data: {e}")
        # noinspection PyTypeChecker,PyUnresolvedReferences
        oapi["paths"][path][method.lower()]["requestBody"] = {
            "content": {media_type: {"schema": _gen_schema(data, example)}}
        }
    else:
        # noinspection PyTypeChecker,PyUnresolvedReferences
        del oapi["paths"][path][method.lower()]["requestBody"]

    if response:
        try:
            data = _load_json_yaml(response)
        except ValueError as e:
            raise ValueError(f"Cannot load response data: {e}")
        if reference:
            schema_name = path.split("/")[-1].capitalize()
            # noinspection PyTypeChecker,PyUnresolvedReferences
            oapi["paths"][path][method.lower()]["responses"][resp_code][
                "content"
            ] = {
                media_type: {
                    "schema": {"$ref": f"#/components/schemas/{schema_name}"}
                }
            }
            # noinspection PyUnresolvedReferences
            oapi["components"] = {
                "schemas": {schema_name: _gen_schema(data, example)}
            }
        else:
            # noinspection PyTypeChecker,PyUnresolvedReferences
            oapi["paths"][path][method.lower()]["responses"][resp_code][
                "content"
            ] = {media_type: {"schema": _gen_schema(data, example)}}

    validate_spec(copy.deepcopy(oapi))

    return oapi
