# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/AsyncAPI_Spec_Generator.ipynb.

# %% auto 0
__all__ = ['logger', 'generate_asyncapi_spec']

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 1
from typing import *
import time
import platform
import subprocess  # nosec: B404: Consider possible security implications associated with the subprocess module.
from pathlib import Path
from tempfile import TemporaryDirectory

from yaspin import yaspin
import yaml
from packaging import version

from fastkafka._components.docs_dependencies import _check_npm_with_local, npm_required_major_version

from .._components.logger import get_logger
from .helper import CustomAIChat, ValidateAndFixResponse, write_file_contents
from .prompts import ASYNCAPI_SPEC_GENERATION_PROMPT
from .constants import ASYNC_API_SPEC_FILE_NAME, MAX_ASYNC_SPEC_RETRIES, MAX_NUM_FIXES_MSG

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 3
logger = get_logger(__name__)

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 5
def _extract_errors(output: str, spec_dir: str) -> List[str]:
    """Extract error messages from the AsyncAPI CLI validation output.

    Args:
        output: The output of the AsyncAPI CLI validation command.
        base_directory (str): The base directory name of the generated AsyncAPI spec file.

    Returns:
        A list of error messages generated by AsyncAPI CLI validation command.
    """
    output_lines = output.split("\n")
    errors = [
        line.replace(f"{spec_dir}/", "") for line in output_lines if " error " in line
    ]
    return errors

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 9
def _validate_response(response: str) -> List[str]:
    """Validate the AsyncAPI spec generated by OpenAI

    Args:
        response: The AsyncAPI spec generated by OpenAI in string format.

    Returns:
        Returns a list of errors if any found during the validation of the spec.

    Raises:
        json.JSONDecodeError: If the response is not a valid JSON.
    """    
    # check if nmp is installed
    try:
        _check_npm_with_local()
    except RuntimeError as e:
        raise RuntimeError(
            f"Error: npm not found. To use the code generation feature, you must have npm >= {npm_required_major_version} installed.\nPlease run the following command to install the required dependencies:\n\nfastkafka docs install_deps"
        )

    with TemporaryDirectory() as d:
        spec_path = Path(d) / ASYNC_API_SPEC_FILE_NAME
        with open(spec_path, "w", encoding="utf-8") as f:
            f.write(response)

        cmd = [
            "npx",
            "-y",
            "-p",
            "@asyncapi/cli",
            "asyncapi",
            "validate",
            f"{spec_path}",
            "--diagnostics-format",
            "text",
        ]
        # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
        p = subprocess.run(  # nosec: B602, B603 subprocess call - check for execution of untrusted input.
            cmd,
            stderr=subprocess.STDOUT,
            stdout=subprocess.PIPE,
            shell=True if platform.system() == "Windows" else False,
        )
        if p.returncode == 0:
            errors = _extract_errors(p.stdout.decode(), str(d))
            return errors
        else:
            logger.info(f"Validation of AsyncAPI spec failed!")
            logger.info(f"Output of '$ {' '.join(cmd)}'{p.stdout.decode()}")
            raise ValueError(
                f"Validation of AsyncAPI spec failed, '$ {' '.join(cmd)}'{p.stdout.decode()}.\n\nPlease try again."
            )

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 15
def _asyncapi_to_latest_version(asyncapi_yaml_path: str) -> None:
    """
    Convert the AsyncAPI specification to the latest version.

    Args:
        asyncapi_yaml_path: The AsyncAPI specification which needs to be converted.
    """
    with open(asyncapi_yaml_path, "r") as stream:
        current_version = yaml.safe_load(stream)["asyncapi"]
        
    LATEST_ASYNCAPI_VERSION = "2.6.0"
    if version.parse(current_version) < version.parse(LATEST_ASYNCAPI_VERSION):
        cmd = [
            "npx",
            "-y",
            "-p",
            "@asyncapi/cli",
            "asyncapi",
            "convert",
            f"{asyncapi_yaml_path}",
            "-t",
            f"{LATEST_ASYNCAPI_VERSION}",
            "-o",
            f"{asyncapi_yaml_path}",
        ]
        # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
        p = subprocess.run(  # nosec: B602, B603 subprocess call - check for execution of untrusted input.
            cmd,
            stderr=subprocess.STDOUT,
            stdout=subprocess.PIPE,
            shell=True if platform.system() == "Windows" else False,
        )
        
        logger.info("Executing 'asyncapi convert' on the generated asyncapi specification file:")
        if p.returncode == 0:
            current_version = LATEST_ASYNCAPI_VERSION
        else:   
            logger.info(f"Issues while executing 'asyncapi convert' command: {p.stdout.decode()}")
            
    logger.info(f"Using AsyncAPI version: {current_version} for the specification creation")

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 17
def _optimize_asyncapi_file(asyncapi_yaml_path: str) -> None:
    """
    Optimize the AsyncAPI specificationn.

    Args:
        asyncapi_yaml_path: The AsyncAPI specification which needs to be optimized.
    """
    cmd = [
        "npx",
        "-y",
        "-p",
        "@asyncapi/cli",
        "asyncapi",
        "optimize",
        f"{asyncapi_yaml_path}",
        "-o",
        "overwrite",
    ]
    # nosemgrep: python.lang.security.audit.subprocess-shell-true.subprocess-shell-true
    p = subprocess.run(  # nosec: B602, B603 subprocess call - check for execution of untrusted input.
        cmd,
        stderr=subprocess.STDOUT,
        stdout=subprocess.PIPE,
        shell=True if platform.system() == "Windows" else False,
    )

    # Note: asyncapi cli is returning incorrect log. 
    # If we want to optimize asyncapi.yml, we will get the message "Created file asyncapi_optimized.yml"
    # asyncapi_optimized.yml ISN'T created - asyncapi.yml was overwritten (we are using attribute -o overwrite)
 
    logger.info("Executing 'asyncapi optimize' on the generated asyncapi specification file:")
    if p.returncode == 0:
        logger.info(p.stdout.decode())
    else:   
        logger.info(f"Issues while executing 'asyncapi optimize' command: {p.stdout.decode()}")

# %% ../../nbs/AsyncAPI_Spec_Generator.ipynb 21
def _generate_asyncapi_spec(description: str, total_usage: List[Dict[str, int]]) -> Tuple[str, List[Dict[str, int]]]:
    async_spec_generator = CustomAIChat(user_prompt=ASYNCAPI_SPEC_GENERATION_PROMPT)
    async_spec_validator = ValidateAndFixResponse(async_spec_generator, _validate_response, max_attempts=3)
    validated_async_spec, total_usage = async_spec_validator.fix(description, total_usage)
    return validated_async_spec, total_usage

def generate_asyncapi_spec(description: str, output_path: str, total_usage: List[Dict[str, int]], max_attempts: int = MAX_ASYNC_SPEC_RETRIES) -> List[Dict[str, int]]:
    """Generate a AsyncAPI spec from the user's application description

    Args:
        description: Validated User application description
        output_path: The path to the output file where the generated AsyncAPI spec will be saved.
        max_attempts: An optional integer specifying the maximum number of attempts to generate asyncapi specification (number of fixes are not included in this number).

    Returns:
        Appends total token used to generate the AsyncAPI spec to the end of total_usage list
    """
    with yaspin(
        text="Generating AsyncAPI specification (usually takes around 15 to 30 seconds)...",
        color="cyan",
        spinner="clock",
    ) as sp:
        iterations: int = 0
        while True:
            logger.info(f"\nGenerating AsyncAPI specification - {iterations + 1}. attempt")      
            try:
                validated_async_spec, total_usage = _generate_asyncapi_spec(description, total_usage)
                break
            except ValueError as e:
                if MAX_NUM_FIXES_MSG not in str(e) or iterations >= max_attempts:
                    raise e
                # Try to generate specifiction from the beginning
                iterations += 1

        output_file = f"{output_path}/{ASYNC_API_SPEC_FILE_NAME}"
        write_file_contents(output_file, validated_async_spec)
        _asyncapi_to_latest_version(output_file)
        _optimize_asyncapi_file(output_file)

        sp.text = ""
        sp.ok(f" ✔ AsyncAPI specification generated and saved to: {output_file}")
        return total_usage
