# *************************************************************************
#
#  Copyright (c) 2026 - Datatailr Inc.
#  All Rights Reserved.
#
#  This file is part of Datatailr and subject to the terms and conditions
#  defined in 'LICENSE.txt'. Unauthorized copying and/or distribution
#  of this file, in parts or full, via any medium is strictly prohibited.
# *************************************************************************

import warnings
from typing import Dict, List, Optional, Union, Callable
from datatailr.build import Image
from datatailr import ACL, Environment, User
from datatailr.scheduler.base import Job, JobType, EntryPoint, Resources


FRAMEWORK_REQUIREMENTS = {
    "streamlit": ["streamlit"],
    "dash": ["dash", "gunicorn"],
    "flask": ["flask", "gunicorn"],
    "fastapi": ["fastapi", "uvicorn", "jinja2", "python-multipart"],
    "panel": ["panel", "gunicorn"],
    "voila": ["voila", "ipywidgets", "jupyter"],
    "jupyter": ["jupyter"],
}


class App(Job):
    """Deploy a web application or dashboard to Datatailr.

    This supports Streamlit, Dash, or any framework that can be started via a
    standard entrypoint (for example, ``streamlit run app.py``).

    Args:
        name: App display name.
        entrypoint: Callable that starts the app server.
        environment: Target environment for the job.
        image: Container image to use for execution.
        run_as: User to execute the job as.
        resources: Resource requirements for the job.
        acl: Access control settings for the job.
        framework: Framework to use for the app.
        python_version: Python version to use.
        python_requirements: Requirements to install (string or list).
        build_script_pre: Shell script to run before build.
        build_script_post: Shell script to run after build.
        env_vars: Environment variables to set.
        update_existing: Update an existing job with the same name.

    Examples:
        Minimal Streamlit app.

        ```python
        # app.py
        import streamlit as st

        def main():
            st.title("Hello Datatailr App")

        if __name__ == "__main__":
            main()
        ```

        Test locally.

        ```bash
        streamlit run app.py
        ```

        Deploy to Datatailr.

        ```python
        from app import main
        from datatailr import App

        app = App(
            name="Simple Dashboard App",
            entrypoint=main,
            python_requirements="streamlit")

        app.run()
        ```
    """

    def __init__(
        self,
        name: str,
        entrypoint: Callable | str,
        environment: Optional[Environment] = Environment.DEV,
        image: Optional[Image] = None,
        run_as: Optional[Union[str, User]] = None,
        resources: Resources = Resources(),
        acl: Optional[ACL] = None,
        framework: Optional[str] = None,
        python_version: str = "3.12",
        python_requirements: str | List[str] = "",
        build_script_pre: str = "",
        build_script_post: str = "",
        env_vars: Optional[Dict[str, str | int | float | bool]] = None,
        update_existing: bool = False,
        app_section: str = "",
    ):
        """Initialize an App deployment.

        Args:
            name: Display name for the app.
            entrypoint: The callable (function) that starts the application.
            environment: Target environment for the deployment.
            image: Pre-configured container Image. When ``None``, an image is
                built from *python_version*, *python_requirements*, and the
                build scripts.
            run_as: User or username to run the app as.  Defaults to the
                currently signed-in user.
            resources: CPU and memory resources for the container.
            acl: Access control list.  Defaults to standard permissions for the
                current user.
            framework: Optional framework name (e.g., 'streamlit', 'dash',
                'flask').  If not provided, the framework is inferred from the
                entrypoint.
            python_version: Python version for the container image.
            python_requirements: Python dependencies (see ``Image``).
            build_script_pre: Dockerfile commands to run before installing
                requirements.
            build_script_post: Dockerfile commands to run after installing
                requirements.
            env_vars: Environment variables passed to the running container.
            update_existing: If ``True``, load and update an existing job
                definition with the same name instead of creating a new one.
            app_section: The section to which the app belongs. If not provided, the app will be assigned to the default section. This affects the app launcher page.
        """
        env_vars = dict(env_vars or {})
        entrypoint = EntryPoint(JobType.APP, entrypoint)
        if framework and framework not in FRAMEWORK_REQUIREMENTS:
            raise ValueError(f"Unsupported framework: {framework}")
        if framework and isinstance(python_requirements, list):
            framework_python_requirements = FRAMEWORK_REQUIREMENTS[framework]
            missing_requirements = set(framework_python_requirements) - set(
                python_requirements
            )
            if missing_requirements:
                warnings.warn(
                    f"Framework {framework} requires additional python requirements: {missing_requirements}",
                    Warning,
                    stacklevel=2,
                )
        if entrypoint.file_path and framework is None:
            framework = "voila"
        if framework and "DATATAILR_FRAMEWORK" not in env_vars:
            env_vars["DATATAILR_FRAMEWORK"] = framework

        super().__init__(
            name=name,
            type=JobType.APP,
            entrypoint=entrypoint,
            environment=environment,
            image=image,
            run_as=run_as,
            resources=resources,
            acl=acl,
            python_version=python_version,
            python_requirements=python_requirements,
            build_script_pre=build_script_pre,
            build_script_post=build_script_post,
            env_vars=env_vars,
            get_existing=update_existing,
            app_section=app_section,
        )


class Service(Job):
    """Represents a long-running background service deployment on Datatailr.

    A service runs continuously (e.g., an API server, a message consumer, or
    any always-on process).  It is restarted automatically if it exits.

    Example:
        ```python
        from datatailr import Service

        # service.py
        from flask import Flask

        app = Flask(__name__)

        @app.route("/__health_check__.html")
        def health_check():
            return "OK"

        # Service entrypoints receive the port from Datatailr.
        def run_server(port):
            app.run("0.0.0.0", port=int(port), debug=False)

        svc = Service(
            name="Simple Service",
            entrypoint=run_server,
            python_requirements=["flask"],
        )
        svc.run()
        ```
    """

    def __init__(
        self,
        name: str,
        entrypoint: Callable,
        environment: Optional[Environment] = Environment.DEV,
        image: Optional[Image] = None,
        run_as: Optional[Union[str, User]] = None,
        resources: Resources = Resources(),
        acl: Optional[ACL] = None,
        python_version: str = "3.12",
        python_requirements: str | List[str] = "",
        build_script_pre: str = "",
        build_script_post: str = "",
        env_vars: Optional[Dict[str, str | int | float | bool]] = None,
        update_existing: bool = False,
    ):
        """Initialize a Service deployment.

        Args:
            name: Display name for the service.
            entrypoint: The callable (function) that starts the service.
            environment: Target environment for the deployment.
            image: Pre-configured container Image.
            run_as: User or username to run the service as.
            resources: CPU and memory resources for the container.
            acl: Access control list.
            python_version: Python version for the container image.
            python_requirements: Python dependencies (see ``Image``).
            build_script_pre: Dockerfile commands to run before installing
                requirements.
            build_script_post: Dockerfile commands to run after installing
                requirements.
            env_vars: Environment variables passed to the running container.
            update_existing: If ``True``, update an existing job definition.
        """
        env_vars = dict(env_vars or {})
        entrypoint = EntryPoint(JobType.SERVICE, entrypoint)
        super().__init__(
            name=name,
            type=JobType.SERVICE,
            entrypoint=entrypoint,
            environment=environment,
            image=image,
            run_as=run_as,
            resources=resources,
            acl=acl,
            python_version=python_version,
            python_requirements=python_requirements,
            build_script_pre=build_script_pre,
            build_script_post=build_script_post,
            env_vars=env_vars,
            get_existing=update_existing,
        )


class ExcelAddin(Job):
    """Represents an Excel add-in deployment on Datatailr.

    An Excel add-in exposes Python functions as Excel worksheet functions,
    allowing users to call server-side computations directly from Excel
    spreadsheets.

    Example:
        ```python
        from datatailr import ExcelAddin
        from datatailr.excel import Addin

        addin_def = Addin("Options Pricer", "Option pricing functions")

        @addin_def.expose(description="Black-Scholes price")
        def price_option(spot, strike, vol, rate, expiry):
            ...

        def __excel_main__(port=8080, ws_port=8000):
            addin_def.run(port, ws_port)

        addin = ExcelAddin(
            name="Options Pricer",
            entrypoint=__excel_main__,
            python_requirements=["numpy", "scipy"],
        )
        addin.run()
        ```
    """

    def __init__(
        self,
        name: str,
        entrypoint: Callable,
        environment: Optional[Environment] = Environment.DEV,
        image: Optional[Image] = None,
        run_as: Optional[Union[str, User]] = None,
        resources: Resources = Resources(),
        acl: Optional[ACL] = None,
        python_version: str = "3.12",
        python_requirements: str | List[str] = "",
        build_script_pre: str = "",
        build_script_post: str = "",
        env_vars: Optional[Dict[str, str | int | float | bool]] = None,
        update_existing: bool = False,
        app_section: str = "",
    ):
        """Initialize an Excel add-in deployment.

        Args:
            name: Display name for the add-in.
            entrypoint: The callable (function) that starts the add-in server.
            environment: Target environment for the deployment.
            image: Pre-configured container Image.
            run_as: User or username to run the add-in as.
            resources: CPU and memory resources for the container.
            acl: Access control list.
            python_version: Python version for the container image.
            python_requirements: Python dependencies (see ``Image``).
            build_script_pre: Dockerfile commands to run before installing
                requirements.
            build_script_post: Dockerfile commands to run after installing
                requirements.
            env_vars: Environment variables passed to the running container.
            update_existing: If ``True``, update an existing job definition.
            app_section: The section to which the app belongs. If not provided, the app will be assigned to the default section. This affects the app launcher page.
        """
        env_vars = dict(env_vars or {})
        entrypoint = EntryPoint(JobType.EXCEL, entrypoint)
        super().__init__(
            name=name,
            type=JobType.EXCEL,
            entrypoint=entrypoint,
            environment=environment,
            image=image,
            run_as=run_as,
            resources=resources,
            acl=acl,
            python_version=python_version,
            python_requirements=python_requirements,
            build_script_pre=build_script_pre,
            build_script_post=build_script_post,
            env_vars=env_vars,
            get_existing=update_existing,
            app_section=app_section,
        )
