import panel as pn
import os
import re
import warnings
from typing import Literal


def _request_google_token_in_browser(
    oauth_key: str,
    oauth_extra_params: dict | None = None,
) -> str:
    """Request a Google OAuth access token using the browser's OAuth2 implicit flow.

    Only works inside a Pyodide environment. Uses Google Identity Services
    to open a consent popup and return an access token.

    Parameters
    ----------
    oauth_key : str
        Google OAuth Client ID.
    oauth_extra_params : dict, optional
        Extra params; ``scope`` key is used if present.

    Returns
    -------
    str
        OAuth 2.0 access token.
    """
    from js import window, document  # pyright: ignore [reportMissingImports]
    from pyodide.ffi import create_once_callable  # pyright: ignore [reportMissingImports]
    import asyncio

    scope = "https://www.googleapis.com/auth/bigquery"
    if oauth_extra_params and "scope" in oauth_extra_params:
        scope = oauth_extra_params["scope"]

    loop = asyncio.get_event_loop()
    future = loop.create_future()

    def _on_token_response(response):
        future.set_result(response.access_token)

    # Load Google Identity Services script if not already present.
    if not hasattr(window, "google") or not hasattr(window.google, "accounts"):
        script = document.createElement("script")
        script.src = "https://accounts.google.com/gsi/client"
        script.async_ = True
        document.head.appendChild(script)
        # Wait for the script to load.
        load_future = loop.create_future()
        script.onload = create_once_callable(lambda *_: load_future.set_result(None))
        loop.run_until_complete(load_future)

    client = window.google.accounts.oauth2.initTokenClient(
        client_id=oauth_key,
        scope=scope,
        callback=create_once_callable(_on_token_response),
    )
    client.requestAccessToken()
    return loop.run_until_complete(future)


def _extract_head_resources(html: str) -> str:
    """Extract CSS/JS resources from <head>, excluding meta charset and title."""
    head_match = re.search(r'<head>(.*?)</head>', html, re.DOTALL)
    if not head_match:
        raise ValueError("Could not find <head> in generated HTML")
    head_content = head_match.group(1)
    head_content = re.sub(r'\s*<meta\s+charset="[^"]*">\s*', '', head_content)
    head_content = re.sub(r'\s*<title>.*?</title>\s*', '', head_content)
    return head_content.strip()


def _extract_data_roots(html: str) -> str:
    """Extract all <div ... data-root-id="..."> elements from the body."""
    return '\n'.join(re.findall(
        r'<div\s+id="[^"]+"\s+data-root-id="[^"]+"\s+style="[^"]*">\s*</div>',
        html,
    ))


def _extract_worker_script(html: str) -> str:
    """Extract the <script> block containing pyodideWorker from the body."""
    body_match = re.search(r'<body[^>]*>(.*?)</body>', html, re.DOTALL)
    if not body_match:
        raise ValueError("Could not find <body> in generated HTML")
    scripts = re.findall(
        r'(<script\s+type="text/javascript">.*?</script>)',
        body_match.group(1),
        re.DOTALL,
    )
    for script in scripts:
        if 'pyodideWorker' in script:
            return script
    raise ValueError("Could not find pyodideWorker script in generated HTML")


def _modify_worker_script(script: str) -> str:
    """Modify the worker handler script to target #pn-loading and #pn-app."""
    old_status = (
        "let loading_msg\n"
        "          if (loading_msgs.length) {\n"
        "            loading_msg = loading_msgs[0]\n"
        "          } else if (body.classList.contains('pn-loading')) {\n"
        "            loading_msg = document.createElement('div')\n"
        "            loading_msg.classList.add('pn-loading-msg')\n"
        "            body.appendChild(loading_msg)\n"
        "          }\n"
        "          if (loading_msg != null) {\n"
        "            loading_msg.innerHTML = msg.msg\n"
        "          }"
    )
    new_status = (
        "const loadingEl = document.getElementById('pn-loading')\n"
        "          if (loadingEl != null) {\n"
        "            loadingEl.innerHTML = msg.msg\n"
        "          }"
    )
    modified = script.replace(old_status, new_status)
    if modified == script:
        warnings.warn(
            "Could not patch the status handler in the generated JS. "
            "The loading div may not receive status messages.",
            stacklevel=2,
        )

    old_cleanup = (
        '// Remove loading spinner and message\n'
        '          body.classList.remove("pn-loading", "arc")\n'
        '          for (const loading_msg of loading_msgs) {\n'
        '            loading_msg.remove()\n'
        '          }'
    )
    new_cleanup = (
        '// Hide loading, show app\n'
        '          const loadingWrapper = document.getElementById("pn-loading-wrapper")\n'
        '          if (loadingWrapper) { loadingWrapper.style.display = "none" }\n'
        '          const appEl = document.getElementById("pn-app")\n'
        '          if (appEl) { appEl.style.display = "" }\n'
        '          body.classList.remove("pn-loading", "arc")'
    )
    modified2 = modified.replace(old_cleanup, new_cleanup)
    if modified2 == modified:
        warnings.warn(
            "Could not patch the render cleanup in the generated JS. "
            "The loading/app transition may not work as expected.",
            stacklevel=2,
        )

    return modified2


def _apply_template(generated_html_path: str, template_path: str) -> None:
    """Replace generated HTML with user's template, injecting Panel resources."""
    with open(generated_html_path) as f:
        generated = f.read()
    with open(template_path) as f:
        template = f.read()

    head_resources = _extract_head_resources(generated)
    data_roots = _extract_data_roots(generated)
    worker_script = _extract_worker_script(generated)
    worker_script = _modify_worker_script(worker_script)

    app_content = f"{data_roots}\n{worker_script}"

    # Inject head resources
    if '<!-- pn-head -->' in template:
        result = template.replace('<!-- pn-head -->', head_resources)
    elif '</head>' in template:
        result = template.replace('</head>', f"{head_resources}\n</head>")
    else:
        raise ValueError("Template has no <head> section")

    # Inject app content into #pn-app or before </body>
    pn_app_match = re.search(
        r'(<div\s+id="pn-app"[^>]*>)\s*(</div>)', result, re.DOTALL,
    )
    if pn_app_match:
        result = result.replace(
            pn_app_match.group(0),
            f"{pn_app_match.group(1)}\n{app_content}\n{pn_app_match.group(2)}",
        )
    elif '</body>' in result:
        warnings.warn(
            "Template has no #pn-app div; injecting app content before </body>.",
            stacklevel=2,
        )
        result = result.replace('</body>', f"{app_content}\n</body>")
    else:
        raise ValueError("Template has no </body> tag")

    if 'id="pn-loading"' not in template:
        warnings.warn(
            "Template has no #pn-loading div; loading messages will not be displayed.",
            stacklevel=2,
        )
    if 'id="pn-loading-wrapper"' not in template:
        warnings.warn(
            "Template has no #pn-loading-wrapper div; the loading section will not be hidden after load.",
            stacklevel=2,
        )

    with open(generated_html_path, 'w') as f:
        f.write(result)


def share(
    view,
    __name__,
    __file__,
    sharer: Literal["pn.serve", "embed.html", "pyodide"] | int | None = 0,
    template: str | None = None,
    pyodide_kwargs={"runtime": "pyodide-worker", "prerender": False, "inline": False},
    panel_serve_kwargs={"port": 8000, "show": True},
    oauth_provider: str | None = None,
    oauth_key: str | None = None,
    oauth_secret: str | None = None,
    cookie_secret: str | None = None,
    oauth_encryption_key: str | None = None,
    oauth_extra_params: dict | None = None,
    oauth_refresh_tokens: bool = False,
    access_token: str | None = None,
):

    if __name__ == "__main__":
        if isinstance(sharer, int):
            sharer = ["pn.serve", "embed.html", "pyodide", None][sharer]
        if pn.state._is_pyodide:
            if access_token:
                pn.state.access_token = access_token
            elif oauth_key:
                pn.state.access_token = _request_google_token_in_browser(
                    oauth_key, oauth_extra_params,
                )
            return view.servable()
        elif sharer == "pyodide":
            import panel.io.convert
            import http.server
            import socketserver
            import webbrowser

            def start_http_server(port=8000, directory=".", show=True, fn=""):
                """
                Start an HTTP server to serve files from a specific directory.

                :param port: The port to serve on (default is 8000).
                :param directory: The directory to serve files from (default is the current directory).
                """
                # Change the working directory to the specified folder
                current_workdir = os.getcwd()
                os.chdir(directory)

                # Set up the HTTP server
                handler = http.server.SimpleHTTPRequestHandler
                with socketserver.TCPServer(("", port), handler) as httpd:
                    print(f"Serving HTTP on port {port} (http://localhost:{port}/) ...")
                    print(f"Serving files from directory: {os.path.abspath(directory)}")
                    try:
                        if show:
                            webbrowser.open(f"http://localhost:{port}/{fn}")
                        httpd.serve_forever()
                    except KeyboardInterrupt:
                        print("\nShutting down the server.")
                        httpd.server_close()
                        os.chdir(current_workdir)

            source_file = os.path.abspath(__file__)
            assert os.path.isfile(source_file), f"Can't find file `{__file__}`."
            workdir = os.path.join(os.path.split(__file__)[0], "dist")
            if not os.path.isdir(workdir):
                os.mkdir(workdir)
            _ = panel.io.convert.convert_app(
                source_file,
                dest_path=workdir,
                **pyodide_kwargs,
            )
            if template is not None:
                generated_html = os.path.join(
                    workdir,
                    os.path.split(source_file)[1].replace(".py", ".html"),
                )
                _apply_template(generated_html, template)
            start_http_server(
                directory=workdir,
                fn=os.path.split(source_file)[1].replace(".py", ".html"),
            )
        elif sharer == "pn.serve":
            if "server" in globals():
                globals()["server"].stop()
            oauth_kwargs = {}
            if oauth_provider:
                oauth_kwargs["oauth_provider"] = oauth_provider
            if oauth_key:
                oauth_kwargs["oauth_key"] = oauth_key
            if oauth_secret:
                oauth_kwargs["oauth_secret"] = oauth_secret
            if cookie_secret:
                oauth_kwargs["cookie_secret"] = cookie_secret
            if oauth_encryption_key:
                oauth_kwargs["oauth_encryption_key"] = oauth_encryption_key
            if oauth_extra_params:
                oauth_kwargs["oauth_extra_params"] = oauth_extra_params
            if oauth_refresh_tokens:
                oauth_kwargs["oauth_refresh_tokens"] = oauth_refresh_tokens
            server: pn.io.server.Server = pn.serve(
                view, **panel_serve_kwargs, **oauth_kwargs,
            )
            globals()["server"] = server
        elif sharer == "embed.html":
            fn = os.path.split(os.path.abspath(__file__))[-1].replace(".py", ".html")
            view.save(fn, embed=True, max_opts=50, embed_json=False)
        else:
            ...
    else:
        return view.servable()
