import os
import json

import yaml
from jsonschema import validate, ValidationError
from json_ref_dict import materialize, RefDict
from openapi_spec_validator import validate as validate_spec
from openapi_spec_validator.exceptions import OpenAPISpecValidatorError

from .errors import *


def parse_steps_file(step_file_path: str) -> dict:
    """Parse a .yml steps file and convert it to a python dictionary

    Args:
        step_file_path (str): Relative path to steps file

    Raises:
        FileNotFoundError: Step file not found
        ValidationError: Step file not valid

    Returns:
        dict: Step file converted to a python dictionary
    """
    try:
        steps_schema_path = os.path.join(os.path.dirname(__file__), "steps_schema.yml")
        with open(steps_schema_path, "r") as step_schema_file:
            steps_schema = yaml.safe_load(step_schema_file.read())
    except FileNotFoundError as e:
        raise FileNotFoundError("Steps schema file not found") from e

    try:
        with open(step_file_path, "r") as step_file:
            steps = yaml.safe_load(step_file.read())
            validate(instance=steps, schema=steps_schema)

    except ValidationError as e:
        raise ValidationError(
            f"Steps file has the following syntax errors: {e.message} "
        ) from e
    except FileNotFoundError as e:
        raise FileNotFoundError(f"Steps file {step_file_path} not found") from e

    print("[bold green]--Successfully Validated Steps File--[/bold green]")
    return steps


def parse_spec_file(steps: dict, spec_file_path: str) -> tuple:
    """Parse a valid OpenAPI document into a python dictionary

    Args:
        spec_file_path (str): Relative path to OpenAPI spec file

    Raises:
        OpenAPIValidationError: Invalid API Definition
        FileNotFoundError: Spec file not found
        UnsupportedSchemaError: Schema not supported

    Returns:
        tuple: Returns a tuple containing two dictionaries - the parsed API spec and
            a dictionary with all the openapi operations for quick reference
    """
    try:
        spec = materialize(RefDict(spec_file_path))
        validate_spec(spec)
    except OpenAPISpecValidatorError as e:
        raise OpenAPISpecValidatorError(
            f"API Spec file has the following syntax errors: {e} "
        ) from e
    except ValidationError as e:
        raise ValidationError(
            f"API Spec file has the following syntax errors: {e.message} "
        ) from e
    except FileNotFoundError as e:
        raise FileNotFoundError(f"API Spec file {spec_file_path} not found") from e

    # Get list of steps operationIds
    steps_operation_id_list = []
    for step in steps["steps"]:
        steps_operation_id_list.append(step["operation_id"])

    # Create operation schemas dict for easier parsing
    operation_schemas: dict = {}
    for path in spec["paths"]:
        for method in spec["paths"][path]:
            op_id = spec["paths"][path][method]["operationId"]
            if op_id in steps_operation_id_list:
                operation = spec["paths"][path][method]
                operation_schemas[op_id] = {}

                # Parse and Validate Request Bodies
                if "requestBody" in operation.keys():
                    # TODO: Support multiple content types if one of them is JSON
                    if len(operation["requestBody"]["content"]) > 1:
                        if (
                            "application/json"
                            not in operation["responses"][status_code]["content"]
                        ):
                            raise UnsupportedSchemaError(
                                f"There are too many request body content types for"
                                f" the operation: {op_id}"
                            )
                    if "application/json" in operation["requestBody"]["content"].keys():
                        operation_schemas[op_id]["requestBody"] = operation[
                            "requestBody"
                        ]["content"]["application/json"]["schema"]
                    elif (
                        "application/octet-stream"
                        in operation["requestBody"]["content"].keys()
                    ):
                        operation_schemas[op_id]["requestBody"] = operation[
                            "requestBody"
                        ]["content"]["application/octet-stream"]["schema"]
                    else:
                        raise UnsupportedSchemaError(
                            f"request body content type: "
                            f"{list(operation['requestBody']['content'].keys())[0]}"
                            f" unsupported for the operation: {op_id}"
                        )

                # Parse and Validate Response Bodies
                operation_schemas[op_id]["responses"] = {}
                for status_code in operation["responses"].keys():
                    if "content" in operation["responses"][status_code].keys():
                        # TODO: Support multiple content types if one of them is JSON
                        if len(operation["responses"][status_code]["content"]) > 1:
                            if (
                                "application/json"
                                not in operation["responses"][status_code]["content"]
                            ):
                                raise UnsupportedSchemaError(
                                    f"There are too many response body content types for"
                                    f" the operation: {op_id}"
                                )
                        if (
                            "application/json"
                            in operation["responses"][status_code]["content"].keys()
                        ):
                            operation_schemas[op_id]["responses"][
                                status_code
                            ] = operation["responses"][status_code]["content"][
                                "application/json"
                            ][
                                "schema"
                            ]
                        elif (
                            "application/octet-stream"
                            in operation["responses"][status_code]["content"].keys()
                        ):
                            operation_schemas[op_id]["responses"][
                                status_code
                            ] = operation["responses"][status_code]["content"][
                                "application/octet-stream"
                            ][
                                "schema"
                            ]
                        else:
                            raise UnsupportedSchemaError(
                                f"response body content type: "
                                f"{list(operation['responses'][status_code]['content'].keys())[0]}"
                                f" unsupported for the operation: {op_id}"
                            )
                    else:
                        operation_schemas[op_id]["responses"][status_code] = operation[
                            "responses"
                        ][status_code]

    print("[bold green]--Successfully Validated Spec File--[/bold green]")
    return spec, operation_schemas
