
from __future__ import annotations

import json
import logging
import re
from pathlib import Path

from PySide6.QtWebEngineCore import (
    QWebEngineProfile,
    QWebEngineScript,
)


from config import (
    PLUGIN_BLOCKED_PATTERNS,
    PLUGIN_CRASH_LIMIT,
    PLUGIN_MAX_SIZE,
    PLUGIN_PERMISSIONS,
    profile_extension_dir,
    profile_plugin_dir,
    profile_theme_dir,
    profile_vencord_dir,
)
log = logging.getLogger(__name__)

SCRIPT_WORLD = (
    QWebEngineScript.ScriptWorldId.ApplicationWorld
)

PLUGIN_FILE_PATTERN = (
    "*.plugin.js"
)


def _js_string(
    value: str,
) -> str:
    return json.dumps(
        value,
        ensure_ascii=False,
    )


class ThemeManager:
    def __init__(
        self,
        profile_name: str,
    ):
        self.profile_name = profile_name
        self.directory = profile_theme_dir(
            profile_name
        )

    def discover(
        self,
    ) -> list[Path]:
        return sorted(
            self.directory.glob(
                "*.css"
            )
        )

    def load_scripts(
        self,
    ) -> list[QWebEngineScript]:
        scripts = []

        for css_file in self.discover():
            try:
                css = css_file.read_text(
                    encoding="utf-8"
                )

                script = (
                    QWebEngineScript()
                )

                script.setName(
                    f"theme:{css_file.stem}"
                )

                script.setRunsOnSubFrames(
                    True
                )

                script.setInjectionPoint(
                    QWebEngineScript.InjectionPoint.DocumentReady
                )

                script.setWorldId(
                    SCRIPT_WORLD
                )

                script.setSourceCode(
                    f"""
(() => {{
    const id = "theme-{css_file.stem}";
    if (document.getElementById(id)) return;

    const style = document.createElement("style");
    style.id = id;
    style.textContent = {_js_string(css)};

    const install = () => {{
        if (!document.head) {{
            requestAnimationFrame(install);
            return;
        }}

        document.head.appendChild(style);
    }};

    install();
}})();
"""
                )

                scripts.append(
                    script
                )

            except Exception:
                log.exception(
                    "theme load failed: %s",
                    css_file,
                )

        return scripts


class ExtensionManager:
    def __init__(
        self,
        profile_name: str,
    ):
        self.profile_name = profile_name
        self.directory = (
            profile_extension_dir(
                profile_name
            )
        )

    def discover(
        self,
    ) -> list[Path]:
        if not self.directory.exists():
            return []

        return sorted(
            path
            for path in self.directory.iterdir()
            if path.is_dir()
        )

    def load_scripts(
        self,
    ) -> list[QWebEngineScript]:
        scripts = []

        for extension in self.discover():
            manifest_file = (
                extension
                / "manifest.json"
            )

            script_file = (
                extension
                / "script.js"
            )

            if (
                not manifest_file.exists()
                or not script_file.exists()
            ):
                continue

            try:
                manifest = json.loads(
                    manifest_file.read_text(
                        encoding="utf-8"
                    )
                )

                if not manifest.get(
                    "enabled",
                    True,
                ):
                    continue

                source = (
                    script_file.read_text(
                        encoding="utf-8"
                    )
                )

                script = (
                    QWebEngineScript()
                )

                script.setName(
                    manifest.get(
                        "name",
                        extension.name,
                    )
                )

                script.setRunsOnSubFrames(
                    True
                )

                script.setInjectionPoint(
                    QWebEngineScript.InjectionPoint.DocumentCreation
                )

                script.setWorldId(
                    SCRIPT_WORLD
                )

                script.setSourceCode(
                    source
                )

                scripts.append(
                    script
                )

            except Exception:
                log.exception(
                    "extension load failed: %s",
                    extension,
                )

        return scripts



class PluginManager:
    def __init__(
        self,
        profile_name: str,
        db=None,
    ):
        self.profile_name = profile_name
        self.db = db
        self.directory = profile_plugin_dir(
            profile_name
        )

        self.directory.mkdir(
            parents=True,
            exist_ok=True,
        )
        self.quarantine_dir = (
            profile_vencord_dir(
                profile_name
            )
            / "quarantine"
        )

        self.quarantine_dir.mkdir(
            parents=True,
            exist_ok=True,
        )
    def discover(
        self,
    ) -> list[Path]:
        plugins = []

        for plugin in sorted(
            self.directory.glob(
                PLUGIN_FILE_PATTERN
            )
        ):
            try:
                meta = self.metadata(
                    plugin
                )

                if (
                    self.db
                    and not self.db.plugin_enabled(
                        self.profile_name,
                        meta["name"],
                        True,
                    )
                ):
                    continue

                plugins.append(
                    plugin
                )

            except Exception:
                log.exception(
                    "plugin discovery failed: %s",
                    plugin,
                )

        return plugins


    def metadata(
        self,
        plugin_file: Path,
    ) -> dict:
        source = plugin_file.read_text(
            encoding="utf-8",
            errors="ignore",
        )
        permissions = set()

        for match in re.findall(
            r"@permissions\s+(.+)",
            source,
            re.IGNORECASE,
        ):
            permissions.update(
                token.strip().lower()
                for token in match.split()
                if token.strip()
            )

        permissions &= PLUGIN_PERMISSIONS

        metadata["permissions"] = sorted(
            permissions
        )
        name = plugin_file.name

        if name.endswith(
            ".plugin.js"
        ):
            name = name[:-10]

        metadata = {
            "name": name,
            "version": "unknown",
            "author": "",
            "description": "",
            "enabled": True,
            "path": str(
                plugin_file
            ),
            "size": plugin_file.stat().st_size,
        }

        patterns = {
            "name": r"@name\s+(.+)",
            "version": r"@version\s+(.+)",
            "author": r"@author\s+(.+)",
            "description": r"@description\s+(.+)",
        }

        for key, pattern in (
            patterns.items()
        ):
            match = re.search(
                pattern,
                source,
                re.IGNORECASE,
            )

            if match:
                metadata[key] = (
                    match.group(1)
                    .strip()
                )

        if self.db:
            metadata[
                "enabled"
            ] = self.db.plugin_enabled(
                self.profile_name,
                metadata["name"],
                True,
            )

        return metadata

    def validate_file(
        self,
        source: Path,
    ) -> tuple[
        bool,
        str,
    ]:
        filename = (
            source.name.lower()
        )

        if not filename.endswith(
            ".plugin.js"
        ):
            return (
                False,
                "invalid extension",
            )

        if (
            source.stat().st_size
            > PLUGIN_MAX_SIZE
        ):
            return (
                False,
                "plugin exceeds size limit",
            )

        content = (
            source.read_text(
                encoding="utf-8",
                errors="ignore",
            )
        )

        lowered = (
            content.lower()
        )

        for pattern in (
            PLUGIN_BLOCKED_PATTERNS
        ):
            if (
                pattern.lower()
                in lowered
            ):
                return (
                    False,
                    f"blocked pattern: {pattern}",
                )

        return (
            True,
            "",
        )

    def bootstrap_script(
        self,
    ) -> QWebEngineScript:
        script = (
            QWebEngineScript()
        )

        script.setName(
            "plugin-runtime"
        )

        script.setRunsOnSubFrames(
            True
        )

        script.setInjectionPoint(
            QWebEngineScript.InjectionPoint.DocumentCreation
        )

        script.setWorldId(
            SCRIPT_WORLD
        )

        script.setSourceCode(
            """

(() => {

    if (window.PluginRuntime)
        return;

    window.__bridgeReady =
        new Promise(resolve => {

            const complete = () => {

                if (
                    window.bridge
                ) {
                    resolve(
                        window.bridge
                    );

                    return;
                }

                setTimeout(
                    complete,
                    25,
                );
            };

            if (
                typeof qt !== "undefined"
                && typeof QWebChannel !== "undefined"
            ) {
                new QWebChannel(
                    qt.webChannelTransport,
                    channel => {

                        window.bridge =
                            channel.objects.bridge;

                        resolve(
                            window.bridge
                        );
                    }
                );

                return;
            }

            complete();
        });

    window.PluginRuntime = {
        plugins:{},
        failed:{},
        started:{},
        hasPermission(
            plugin,
            permission
        ){
            return !!(
                this.permissions[
                    plugin
                ]?.[permission]
            );
        },
        async bridge(){
            return await (
                window.__bridgeReady
            );
        },

        log(name,msg){
            console.log(
                `[Plugin:${name}]`,
                msg
            );
        },

        async fail(
            name,
            error
        ){
            console.error(
                `[Plugin:${name}]`,
                error
            );

            this.failed[name] =
                String(error);

            try{
                const bridge =
                    await this.bridge();

                bridge.pluginCrash(
                    name,
                    String(error)
                );
            }
            catch{}
        }
    };

    window.BdApi = {

        Data:{

            async load(
                plugin,
                key
            ){
                try{
                    const bridge =
                        await PluginRuntime.bridge();

                    const raw =
                        bridge.pluginLoad(
                            plugin,
                            key
                        );

                    return JSON.parse(
                        raw
                    );
                }
                catch{
                    return null;
                }
            },

            async save(
                plugin,
                key,
                value
            ){
                try{
                    const bridge =
                        await PluginRuntime.bridge();

                    bridge.pluginSave(
                        plugin,
                        key,
                        JSON.stringify(
                            value
                        )
                    );
                }
                catch{}
            }
        },

        Logger:{

            async log(
                ...a
            ){
                try{
                    const bridge =
                        await PluginRuntime.bridge();

                    bridge.pluginLog(
                        "runtime",
                        "info",
                        a.join(" ")
                    );
                }
                catch{}

                console.log(
                    ...a
                );
            },

            async warn(
                ...a
            ){
                try{
                    const bridge =
                        await PluginRuntime.bridge();

                    bridge.pluginLog(
                        "runtime",
                        "warning",
                        a.join(" ")
                    );
                }
                catch{}

                console.warn(
                    ...a
                );
            },

            async error(
                ...a
            ){
                try{
                    const bridge =
                        await PluginRuntime.bridge();

                    bridge.pluginLog(
                        "runtime",
                        "error",
                        a.join(" ")
                    );
                }
                catch{}

                console.error(
                    ...a
                );
            }
        },

        UI:{
            showToast(
                message
            ){
                console.log(
                    "[Toast]",
                    message
                );
            }
        },

        DOM:{
            addStyle(
                id,
                css
            ){
                let style =
                    document.getElementById(
                        id
                    );

                if(!style){
                    style =
                        document.createElement(
                            "style"
                        );

                    style.id =
                        id;

                    document.head.appendChild(
                        style
                    );
                }

                style.textContent =
                    css;
            },

            removeStyle(
                id
            ){
                document
                    .getElementById(
                        id
                    )
                    ?.remove();
            }
        }
    };

    window.ZeresPluginLibrary = {
        Logger:
            BdApi.Logger,

        Utilities:{},

        DOMTools:
            BdApi.DOM,

        PluginUpdater:{
            checkForUpdate(){}
        },

        Patcher:{
            before(){},
            after(){},
            instead(){},
            unpatchAll(){}
        }
    };
})();
"""
        )

        return script

    def plugin_script(
        self,
        plugin_file: Path,
    ) -> QWebEngineScript:
        meta = self.metadata(
            plugin_file
        )

        source = (
            plugin_file.read_text(
                encoding="utf-8",
                errors="ignore",
            )
        )

        wrapped = f"""
(() => {{
    const module = {{
        exports:null
    }};

    const exports =
        module.exports;

    try {{
        {source}

        const PluginClass =
            module.exports;

        if(
            typeof PluginClass
            !== "function"
        )
            return;

        const plugin =
            new PluginClass();
        plugin.__permissions =
        Object.freeze(
            %PERMISSIONS%
        );

        PluginRuntime.plugins[
            {_js_string(meta["name"])}
        ] = plugin;

        try {{
            if(
                typeof plugin.start
                === "function"
            )
                plugin.start();

            else if(
                typeof plugin.onStart
                === "function"
            )
                plugin.onStart();

            PluginRuntime.started[
                {_js_string(meta["name"])}
            ] = true;
        }}
        catch(error) {{
            PluginRuntime.fail(
                {_js_string(meta["name"])},
                error
            );
        }}
    }}
    catch(error) {{
        PluginRuntime.fail(
            {_js_string(meta["name"])},
            error
        );
    }}
}})();
"""

        script = (
            QWebEngineScript()
        )

        script.setName(
            f"plugin:{meta['name']}"
        )

        script.setRunsOnSubFrames(
            True
        )

        script.setInjectionPoint(
            QWebEngineScript.InjectionPoint.DocumentCreation
        )

        script.setWorldId(
            SCRIPT_WORLD
        )

        script.setSourceCode(
            wrapped
        )

        return script

    def load_scripts(
        self,
    ) -> list[QWebEngineScript]:
        scripts = [
            self.bootstrap_script()
        ]

        for plugin in (
            self.discover()
        ):
            try:
                scripts.append(
                    self.plugin_script(
                        plugin
                    )
                )

            except Exception:
                log.exception(
                    "plugin load failed: %s",
                    plugin,
                )

                if self.db:
                    self.db.plugin_crash(
                        self.profile_name,
                        plugin.stem,
                        "load failure",
                    )

        return scripts

    def install_file(
        self,
        source: Path,
    ) -> Path:
        valid, error = (
            self.validate_file(
                plugin
            )
        )

        if not valid:
            self.quarantine_plugin(
                plugin
            )
            continue
        if self.is_quarantined(
            meta["name"]
        ):
            continue
        filename = re.sub(
            r"[^a-zA-Z0-9._-]",
            "_",
            source.name,
        )

        destination = (
            self.directory
            / filename
        )

        destination.write_text(
            source.read_text(
                encoding="utf-8",
                errors="ignore",
            ),
            encoding="utf-8",
        )

        if (
            self.db
            and self.db.plugin_crash_count(
                self.profile_name,
                meta["name"],
            )
            >= PLUGIN_CRASH_LIMIT
        ):
            self.quarantine_plugin(
                plugin
            )
            continue

        return destination


    def uninstall(
        self,
        plugin_name: str,
    ) -> bool:
        for plugin in (
            self.directory.glob(
                PLUGIN_FILE_PATTERN
            )
        ):
            meta = self.metadata(
                plugin
            )

            if (
                meta["name"]
                != plugin_name
            ):
                continue

            plugin.unlink(
                missing_ok=True
            )

            if self.db:
                self.db.delete_plugin(
                    self.profile_name,
                    plugin_name,
                )

            return True

        return False

    def enable(
        self,
        plugin_name: str,
    ) -> None:
        if self.db:
            self.db.set_plugin_enabled(
                self.profile_name,
                plugin_name,
                True,
            )

    def disable(
        self,
        plugin_name: str,
    ) -> None:
        if self.db:
            self.db.set_plugin_enabled(
                self.profile_name,
                plugin_name,
                False,
            )

    def inventory(
        self,
    ) -> list[dict]:
        rows = []

        for plugin in sorted(
            self.directory.glob(
                PLUGIN_FILE_PATTERN
            )
        ):
            try:
                rows.append(
                    self.metadata(
                        plugin
                    )
                )

            except Exception:
                log.exception(
                    "inventory failed: %s",
                    plugin,
                )

        return rows

    def diagnostics(
        self,
    ) -> dict:
        plugins = (
            self.inventory()
        )

        return {
            "profile": self.profile_name,
            "plugin_count": len(
                plugins
            ),
            "enabled_count": len(
                [
                    p
                    for p in plugins
                    if p.get(
                        "enabled"
                    )
                ]
            ),
            "disabled_count": len(
                [
                    p
                    for p in plugins
                    if not p.get(
                        "enabled"
                    )
                ]
            ),
            "plugins": plugins,
        }
    def quarantine_plugin(
        self,
        plugin_file: Path,
    ) -> None:

        try:
            target = (
                self.quarantine_dir
                / plugin_file.name
            )

            plugin_file.replace(
                target
            )

            meta = self.metadata(
                target
            )

            if self.db:
                self.db.set_plugin_enabled(
                    self.profile_name,
                    meta["name"],
                    False,
                )

                self.db.set_plugin_quarantined(
                    self.profile_name,
                    meta["name"],
                    True,
                )

        except Exception:
            log.exception(
                "plugin quarantine failed"
            )


    def is_quarantined(
        self,
        plugin_name: str,
    ) -> bool:

        if not self.db:
            return False

        return self.db.plugin_quarantined(
            self.profile_name,
            plugin_name,
        )
    # v8

class AddonManager:
    def __init__(
        self,
        profile_name: str,
        db=None,
    ):
        self.profile_name = profile_name
        self.db = db

        self.themes = ThemeManager(
            profile_name
        )

        self.extensions = (
            ExtensionManager(
                profile_name
            )
        )

        self.plugins = (
            PluginManager(
                profile_name,
                db,
            )
        )

    def install(
        self,
        profile: QWebEngineProfile,
    ) -> None:
        collection = (
            profile.scripts()
        )

        existing = {
            collection.script(
                i
            ).name()
            for i in range(
                collection.count()
            )
        }

        for script in (
            self.themes.load_scripts()
        ):
            if (
                script.name()
                not in existing
            ):
                collection.insert(
                    script
                )

        for script in (
            self.extensions.load_scripts()
        ):
            if (
                script.name()
                not in existing
            ):
                collection.insert(
                    script
                )

        for script in (
            self.plugins.load_scripts()
        ):
            if (
                script.name()
                not in existing
            ):
                collection.insert(
                    script
                )

    def inventory(
        self,
    ) -> dict:
        themes = []

        extensions = []

        plugins = (
            self.plugins.inventory()
        )

        for theme in (
            self.themes.discover()
        ):
            themes.append(
                {
                    "name": theme.stem,
                    "type": "theme",
                    "enabled": True,
                    "path": str(
                        theme
                    ),
                }
            )

        for extension in (
            self.extensions.discover()
        ):
            manifest = (
                extension
                / "manifest.json"
            )

            enabled = True

            try:
                if (
                    manifest.exists()
                ):
                    enabled = bool(
                        json.loads(
                            manifest.read_text(
                                encoding="utf-8"
                            )
                        ).get(
                            "enabled",
                            True,
                        )
                    )
            except Exception:
                enabled = False

            extensions.append(
                {
                    "name": extension.name,
                    "type": "extension",
                    "enabled": enabled,
                    "path": str(
                        extension
                    ),
                }
            )

        return {
            "themes": themes,
            "extensions": extensions,
            "plugins": plugins,
        }

    def diagnostics(
        self,
    ) -> dict:
        plugin_diag = (
            self.plugins.diagnostics()
        )

        return {
            "profile": self.profile_name,
            "themes": len(
                self.themes.discover()
            ),
            "extensions": len(
                self.extensions.discover()
            ),
            "plugins": plugin_diag,
        }

    def reload(
        self,
        profile: QWebEngineProfile,
    ) -> None:
        profile.scripts().clear()
        self.install(
            profile
        )

    def install_plugin(
        self,
        source: Path,
    ) -> Path:
        return (
            self.plugins.install_file(
                source
            )
        )

    def uninstall_plugin(
        self,
        plugin_name: str,
    ) -> bool:
        return (
            self.plugins.uninstall(
                plugin_name
            )
        )

    def enable_plugin(
        self,
        plugin_name: str,
    ) -> None:
        self.plugins.enable(
            plugin_name
        )

    def disable_plugin(
        self,
        plugin_name: str,
    ) -> None:
        self.plugins.disable(
            plugin_name
        )

    def plugin_inventory(
        self,
    ) -> list[dict]:
        return (
            self.plugins.inventory()
        )

    def plugin_diagnostics(
        self,
    ) -> dict:
        return (
            self.plugins.diagnostics()
        )

