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

# %% auto 0
__all__ = ['deploy_package', 'CodeEngine_InvalidPackage', 'create_package', 'CodeEngine_PackageNotFound',
           'CodeEngine_PackageAlreadyDeployed', 'update_package', 'upsert_package', 'remove_javascript_comments',
           'parse_javascript_between_functions', 'parse_javascript_between_module', 'remove_after_closing_parens',
           'parse_javascript', 'parse_python', 'parse_functions_factory', 'extract_functions']

# %% ../../nbs/routes/codeengine_package_analyzer.ipynb 2
import httpx

from domolibrary.client.DomoEntity import DomoEnum
from typing import List, Callable
from functools import partial

import re

import domolibrary.client.get_data as gd
import domolibrary.client.DomoError as dmde
import domolibrary.client.ResponseGetData as rgd
import domolibrary.client.DomoAuth as dmda
# from domolibrary.utils.CodeEngineUtils import (
#     CodeEngineScriptAnalyzer,
#     CodeEnginePackageBuilder
# )
from typing import Dict, Any

# %% ../../nbs/routes/codeengine_package_analyzer.ipynb 5
@gd.route_function
async def deploy_package(
    package_id,
    version,
    auth: dmda.DomoAuth,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    parent_class: str = None,
    debug_num_stacks_to_drop=1,
) -> rgd.ResponseGetData:
    
    if not package_id or not version:
        raise CodeEngine_FunctionCallError(
            message= f"Package ID {package_id or 'not provided '} and version {version or ' not provided  '} must be provided.",
            auth=auth,
        )
    
    url = f"https://{auth.domo_instance}.domo.com/api/codeengine/v2/packages/{package_id}/versions/{version}/release"

    res = await gd.get_data(
        auth=auth,
        url=url,
        method="POST",
        debug_api=debug_api,
        session=session,
        parent_class=parent_class,
        num_stacks_to_drop=debug_num_stacks_to_drop,
    )

    if not res.is_success:
        raise CodeEngine_API_Error(res=res)
    
    return res

# %% ../../nbs/routes/codeengine_package_analyzer.ipynb 7
class CodeEngine_InvalidPackage(dmde.DomoError):
    def __init__(self, message: str, auth: dmda.DomoAuth):
        super().__init__(message=message, domo_instance=auth.domo_instance)

@gd.route_function
async def create_package(
    package: Dict[str, Any] | CodeEnginePackageBuilder,
    auth: dmda.DomoAuth,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    parent_class: str = None,
    debug_num_stacks_to_drop=1,
) -> rgd.ResponseGetData:

    if isinstance(package, dict):
        package = CodeEnginePackageBuilder().from_dict(data=package)
    try:
        package = package.build()

    except Exception as e:
        raise CodeEngine_InvalidPackage(
            message=f"Failed to build package: {str(e)}",
            auth=auth,
        )

    url = f"https://{auth.domo_instance}.domo.com/api/codeengine/v2/packages"

    res = await gd.get_data(
        auth=auth,
        url=url,
        method="POST",
        debug_api=debug_api,
        session=session,
        parent_class=parent_class,
        num_stacks_to_drop=debug_num_stacks_to_drop,
        body=package
    )

    if not res.is_success:
        raise CodeEngine_API_Error(res=res)
    
    return res

# %% ../../nbs/routes/codeengine_package_analyzer.ipynb 10
class CodeEngine_PackageNotFound(dmde.DomoError):
    def __init__(self, message: str, auth: dmda.DomoAuth):
        super().__init__(message=message, domo_instance=auth.domo_instance)

class CodeEngine_PackageAlreadyDeployed(dmde.DomoError):
    def __init__(self, message: str, auth: dmda.DomoAuth):
        super().__init__(message=message, domo_instance=auth.domo_instance)

@gd.route_function
async def update_package(
    package: Dict[str, Any] | CodeEnginePackageBuilder,
    auth: dmda.DomoAuth,
    check_different: bool = True,
    create_new_version: bool = False,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    parent_class: str = None,
    debug_num_stacks_to_drop=1,
) -> rgd.ResponseGetData:

    if isinstance(package, dict):
        package = CodeEnginePackageBuilder().from_dict(data=package)
    try:
        package = package.build()
    except Exception as e:
        raise CodeEngine_InvalidPackage(
            message=f"Failed to build package: {str(e)}",
            auth=auth,
        )

    pkg_id = package['id']
    pkg_ver = package['version']

    # Ensure package exists (let not-found propagate)
    if debug_api:
        print(f"Checking for package with ID {pkg_id}...")
    await get_codeengine_package_by_id(
        pkg_id,
        auth=auth,
        debug_api=debug_api,
        session=session,
        parent_class=parent_class,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
    )

    if not create_new_version:
        if debug_api:
            print(f"Retrieving full package information for ID {pkg_id} and version {pkg_ver}...")
        try:
            codeengine_full_package = await get_codeengine_package_by_id_and_version(
                pkg_id,
                pkg_ver,
                auth=auth,
                params={"parts": "code"},
                debug_api=debug_api,
                session=session,
                parent_class=parent_class,
                debug_num_stacks_to_drop=debug_num_stacks_to_drop,
            )

            if codeengine_full_package.response.get('released') is not None:
                raise CodeEngine_PackageAlreadyDeployed(
                    message=f"Package with ID {pkg_id} and version {pkg_ver} is already deployed",
                    auth=auth,
                )

            if check_different:
                existing = codeengine_full_package.response or {}
                try:
                    new_code = package.get('code') if isinstance(package, dict) else getattr(package, 'code', None)
                except Exception:
                    new_code = None

                if existing.get('code') == new_code:
                    if debug_api:
                        print(f"Package with ID {pkg_id} and version {pkg_ver} is identical to existing; skipping update.")
                    return codeengine_full_package

        except CodeEngine_PackageNotFound:
            # Version not found -> proceed to update (POST)
            pass

        if debug_api:
            print(f"Updating package with ID {pkg_id} and version {pkg_ver}...")

    url = f"https://{auth.domo_instance}.domo.com/api/codeengine/v2/packages"

    res = await gd.get_data(
        auth=auth,
        url=url,
        method="POST",
        debug_api=debug_api,
        session=session,
        parent_class=parent_class,
        num_stacks_to_drop=debug_num_stacks_to_drop,
        body=package,
    )

    if not res.is_success:
        raise CodeEngine_API_Error(res=res)

    return res

# %% ../../nbs/routes/codeengine_package_analyzer.ipynb 13
@gd.route_function
async def upsert_package(
    package: Dict[str, Any] | CodeEnginePackageBuilder,
    auth: dmda.DomoAuth,
    check_different: bool = True,
    create_new_version: bool = False,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    parent_class: str = None,
    debug_num_stacks_to_drop=1,
) -> rgd.ResponseGetData:

    if isinstance(package, dict):
        pkg_builder = CodeEnginePackageBuilder().from_dict(data=package)
    else:
        pkg_builder = package

    try:
        pkg = pkg_builder.build()
    except Exception as e:
        raise CodeEngine_InvalidPackage(
            message=f"Failed to build package: {str(e)}",
            auth=auth,
        )

    pkg_id = pkg['id']
    result = None
    if not pkg_id:
        if debug_api:
            print('No Package ID found, creating new package...')
        result = await create_package(
            pkg,
            auth=auth,
            debug_api=debug_api,
            session=session,
            parent_class=parent_class,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        )
        return result

    try:
        await get_codeengine_package_by_id(
            pkg_id,
            auth=auth,
            debug_api=debug_api,
            session=session,
            parent_class=parent_class,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        )
    except CodeEngine_PackageNotFound:
        result = await create_package(
            pkg,
            auth=auth,
            debug_api=debug_api,
            session=session,
            parent_class=parent_class,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        )
        return result

    result = await update_package(
        pkg_builder,
        auth=auth,
        check_different=check_different,
        create_new_version=create_new_version,
        debug_api=debug_api,
            parent_class=parent_class,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        )
    return result


# %% ../../nbs/routes/codeengine_package_analyzer.ipynb 17
def remove_javascript_comments(code, **kwargs):
    pattern = r"(/\*\*(.*?)\*/)"
    return re.sub(pattern, "", code, flags=re.DOTALL)


def parse_javascript_between_functions(code, function_name):
    # pattern = r"((async\s)?function\s+" + function_name + r"\(.*?\).*?)(async\s)?function\s"
    pattern = r"((async\s)?function\s+" + function_name + r"\(.*?\).*?}\n\n\n)"
    match = re.search(pattern, code, re.DOTALL)
    return match.group(0) if match else None


def parse_javascript_between_module(code, function_name):
    pattern = (
        r"((async\s)?function\s+" + function_name + r"\(.*?\).*?)(?=module.export)"
    )
    match = re.search(pattern, code, re.DOTALL)
    return match.group(0) if match else None


def remove_after_closing_parens(code, **kwargs):
    pattern = r"(.*\})"
    match = re.search(pattern, code, re.DOTALL)
    return match.group(0) if match else None


def parse_javascript(function_name, code):
    # remove comments
    code = remove_javascript_comments(code)

    # find everything until the next function definition
    match_str = parse_javascript_between_functions(
        code=code, function_name=function_name
    )

    if not match_str:
        match_str = parse_javascript_between_module(
            code=code, function_name=function_name
        )

    # remove everything after the last closing parens
    return remove_javascript_comments(match_str)


def parse_python(function_name, code):
    pattern = (
        r"(async\s+)?def\s+" + re.escape(function_name) + r"\s*\([^)]*\)\s*:\s*[^}]*"
    )
    match = re.search(pattern, code, re.DOTALL)
    return match.group(0)


class parse_functions_factory(DomoEnum):
    PYTHON = partial(parse_python)
    JAVASCRIPT = partial(parse_javascript)


def extract_functions(
    function_ls: List[dict],  # from API, function metadata
    code: str,  # from API, code string
    language: str,  # PYTHON or JAVASCRIPT, must align with parse_functions_factory enum
    parse_fn: Callable = None,  # function for exctracting functions must receive `code:str` and function_name:str`
):
    """helper function that handles parsing"""

    parse_fn = parse_fn or parse_functions_factory[language]

    res = []

    # return [{ **function, "code" : parse_fn.value(function_name=function["name"], code=code)}
    #     for function in function_ls
    # ]
    for function in function_ls:
        function_name = function["name"]
        # print(function_name)
        code_obj = {"code": parse_fn.value(function_name=function_name, code=code)}
        # print(code_obj)
        res.append({**function, **code_obj})

    return res
