import os
import shutil
import datetime
import json
import time
from nwg_displays.tools import (
    hyprctl,
    niri_msg,
    niri_reload_config,
    save_list_to_text_file,
    save_kdl_output,
    ensure_niri_config_include,
    load_text_file,
    inactive_output_description,
    load_json,
    save_json,
)
from nwg_displays.wallpaper_manager import WallpaperManager
from nwg_displays.tools import get_config

class SettingsApplier:
    @staticmethod
    def apply_from_json(profile_data, outputs_path, config_dir, profile_name):
        """Applies configuration based on a Profile JSON file."""
        SettingsApplier._save_current_state_to_previous_profile(config_dir)

        displays = profile_data["displays"]
        config = profile_data["config"]
        use_desc = config.get("use-desc", False)

        if os.getenv("NIRI_SOCKET"):
            SettingsApplier._apply_niri_json(
                displays, use_desc, outputs_path, profile_data
            )

        elif os.getenv("HYPRLAND_INSTANCE_SIGNATURE"):
            SettingsApplier._apply_hyprland_json(
                displays, use_desc, outputs_path, profile_data
            )

        elif os.getenv("SWAYSOCK"):
            SettingsApplier._apply_sway_json(displays, use_desc)

        SettingsApplier._set_active_profile(config_dir, profile_name)

    @staticmethod
    def _apply_hyprland_json(displays, use_desc, outputs_path, profile_data):
        transforms = {
            "normal": 0,
            "90": 1,
            "180": 2,
            "270": 3,
            "flipped": 4,
            "flipped-90": 5,
            "flipped-180": 6,
            "flipped-270": 7,
        }

        print(f"[Profile] Applying {len(displays)} displays for Hyprland...")
        lines = [SettingsApplier._get_header("Profile Loader")]

        for d in displays:
            if not use_desc:
                name = d["name"]
            else:
                desc_safe = d["description"].replace("#", "##")
                name = f"desc:{desc_safe}"

            if not d["active"]:
                lines.append(f"monitor={name},disable")
                hyprctl(f"dispatch dpms off {d['name']}")
                continue

            line = "monitor={},{}x{}@{},{}x{},{}".format(
                name,
                d["physical_width"],
                d["physical_height"],
                d["refresh"],
                d["x"],
                d["y"],
                d["scale"],
            )

            if d.get("mirror"):
                line += ",mirror,{}".format(d["mirror"])

            if d.get("ten_bit"):
                line += ",bitdepth,10"

            lines.append(line)

            if d["transform"] != "normal":
                t_code = transforms.get(d["transform"], 0)
                lines.append(f"monitor={name},transform,{t_code}")

            cmd = "on" if d["dpms"] else "off"
            hyprctl(f"dispatch dpms {cmd} {d['name']}")

        save_list_to_text_file(lines, outputs_path)
        hyprctl("reload")

        config, config_file = get_config()

        if "wallpapers" in profile_data and config.get(
            "profile-bound-wallpapers", True
        ):
            print("[Profile] Applying wallpapers...")
            time.sleep(1)
            WallpaperManager.apply_wallpapers(profile_data["wallpapers"])

    @staticmethod
    def _apply_niri_json(displays, use_desc, outputs_path, profile_data):
        """Apply niri configuration by writing monitor.kdl file"""
        print(f"[Profile] Applying {len(displays)} displays for niri...")
        
        kdl_data = []
        for d in displays:
            name = d["name"]
            
            display_config = {
                "name": name,
                "active": d["active"],
                "physical_width": d["physical_width"],
                "physical_height": d["physical_height"],
                "refresh": d["refresh"],
                "x": d["x"],
                "y": d["y"],
                "scale": d["scale"],
                "transform": d["transform"],
                "adaptive_sync": d.get("adaptive_sync", False)
            }
            kdl_data.append(display_config)
        
        # Save to monitor.kdl
        save_kdl_output(kdl_data, outputs_path)
        
        # Ensure config.kdl includes monitor.kdl
        niri_config_dir = os.path.dirname(outputs_path)
        ensure_niri_config_include(niri_config_dir, outputs_path)
        
        # Reload niri configuration
        niri_reload_config()
        
        config, config_file = get_config()
        
        if "wallpapers" in profile_data and config.get(
            "profile-bound-wallpapers", True
        ):
            print("[Profile] Applying wallpapers...")
            import time
            time.sleep(1)
            WallpaperManager.apply_wallpapers(profile_data["wallpapers"])

    @staticmethod
    def _apply_sway_json(displays, use_desc):
        from i3ipc import Connection

        cmds = []
        for d in displays:
            name = d["description"] if use_desc else d["name"]
            if not d["active"]:
                cmds.append(f'output "{name}" disable')
                continue

            cmd = 'output "{}"'.format(name)
            custom = "--custom" if d["custom_mode"] else ""
            cmd += " mode {} {}x{}@{}Hz".format(
                custom, d["physical_width"], d["physical_height"], d["refresh"]
            )
            cmd += " pos {} {}".format(d["x"], d["y"])
            cmd += " transform {}".format(d["transform"])
            cmd += " scale {}".format(d["scale"])

            if d.get("scale_filter"):
                cmd += " scale_filter {}".format(d["scale_filter"])

            a_s = "on" if d["adaptive_sync"] else "off"
            cmd += " adaptive_sync {}".format(a_s)

            dpms = "on" if d["dpms"] else "off"
            cmd += " dpms {}".format(dpms)
            cmds.append(cmd)

        i3 = Connection()
        for cmd in cmds:
            i3.command(cmd)

    @staticmethod
    def apply_from_gui(
        display_buttons,
        outputs_activity,
        outputs_path,
        use_desc=False,
        create_confirm_win_callback=None,
        config_dir=None,
        profile_name=None,
    ):
        """
        Applies configuration based on GUI buttons state.
        Refactored from original 'apply_settings'.
        """
        if config_dir:
            SettingsApplier._save_current_state_to_previous_profile(config_dir)

        if os.getenv("NIRI_SOCKET"):
            print(f"[DEBUG] Applying niri config to {outputs_path}")
            SettingsApplier._apply_niri_gui(
                display_buttons,
                outputs_activity,
                outputs_path,
                use_desc,
                create_confirm_win_callback,
                config_dir,
                profile_name,
            )

        elif os.getenv("SWAYSOCK"):
            SettingsApplier._apply_sway_gui(
                display_buttons,
                outputs_activity,
                outputs_path,
                use_desc,
                create_confirm_win_callback,
                config_dir,
                profile_name,
            )

        elif os.getenv("HYPRLAND_INSTANCE_SIGNATURE"):
            SettingsApplier._apply_hyprland_gui(
                display_buttons,
                outputs_activity,
                outputs_path,
                use_desc,
                create_confirm_win_callback,
                config_dir,
                profile_name,
            )
        else:
            print("[Error] No compositor detected (Sway/Hyprland/Niri)")

        if config_dir and profile_name:
            SettingsApplier._set_active_profile(config_dir, profile_name)

    @staticmethod
    def _apply_sway_gui(
        display_buttons,
        outputs_activity,
        outputs_path,
        use_desc,
        create_confirm_win_callback,
        config_dir=None,
        profile_name=None,
    ):
        from i3ipc import Connection

        lines = [SettingsApplier._get_header()]
        cmds = []
        db_names = []

        for db in display_buttons:
            name = db.name if not use_desc else db.description
            db_names.append(name)

            lines.append('output "%s" {' % name)
            cmd = 'output "{}"'.format(name)

            custom_mode_str = "--custom" if db.custom_mode else ""
            lines.append(
                "    mode {} {}x{}@{}Hz".format(
                    custom_mode_str,
                    db.physical_width,
                    db.physical_height,
                    db.refresh,
                )
            )
            cmd += " mode {} {}x{}@{}Hz".format(
                custom_mode_str, db.physical_width, db.physical_height, db.refresh
            )

            lines.append("    pos {} {}".format(db.x, db.y))
            cmd += " pos {} {}".format(db.x, db.y)

            lines.append("    transform {}".format(db.transform))
            cmd += " transform {}".format(db.transform)

            lines.append("    scale {}".format(db.scale))
            cmd += " scale {}".format(db.scale)

            lines.append("    scale_filter {}".format(db.scale_filter))
            cmd += " scale_filter {}".format(db.scale_filter)

            a_s = "on" if db.adaptive_sync else "off"
            lines.append("    adaptive_sync {}".format(a_s))
            cmd += " adaptive_sync {}".format(a_s)

            dpms = "on" if db.dpms else "off"
            lines.append("    dpms {}".format(dpms))
            cmd += " dpms {}".format(dpms)

            lines.append("}")
            cmds.append(cmd)

        if not use_desc:
            for key in outputs_activity:
                if key not in db_names:
                    lines.append('output "{}" disable'.format(key))
                    cmds.append('output "{}" disable'.format(key))
        else:
            for key in outputs_activity:
                desc = inactive_output_description(key)
                if desc not in db_names:
                    lines.append('output "{}" disable'.format(desc))
                    cmds.append('output "{}" disable'.format(desc))

        if os.path.isfile(outputs_path):
            backup = load_text_file(outputs_path).splitlines()
        else:
            backup = []

        save_list_to_text_file(lines, outputs_path)

        i3 = Connection()
        for cmd in cmds:
            i3.command(cmd)

        if create_confirm_win_callback:
            create_confirm_win_callback(backup, outputs_path, config_dir, profile_name)

    @staticmethod
    def _apply_niri_gui(
        display_buttons,
        outputs_activity,
        outputs_path,
        use_desc,
        create_confirm_win_callback,
        config_dir=None,
        profile_name=None,
    ):
        """Apply niri configuration from GUI by writing monitor.kdl file"""
        print(f"[niri] Applying {len(display_buttons)} displays...")
        
        # Save backup BEFORE applying new settings
        backup_path = outputs_path + ".bak"
        if os.path.isfile(outputs_path):
            shutil.copy2(outputs_path, backup_path)
            print(f"[niri] Backup saved to {backup_path}")
        else:
            backup_path = None
        
        kdl_data = []
        for db in display_buttons:
            display_config = {
                "name": db.name,
                "active": db.name not in outputs_activity or outputs_activity.get(db.name, True),
                "physical_width": db.physical_width,
                "physical_height": db.physical_height,
                "refresh": db.refresh,
                "x": db.x,
                "y": db.y,
                "scale": db.scale,
                "transform": db.transform,
                "adaptive_sync": db.adaptive_sync
            }
            kdl_data.append(display_config)
        
        # Save to monitor.kdl in KDL format
        save_kdl_output(kdl_data, outputs_path)
        
        # Ensure config.kdl includes monitor.kdl
        niri_config_dir = os.path.dirname(outputs_path)
        ensure_niri_config_include(niri_config_dir, outputs_path)
        
        # Reload niri configuration
        niri_reload_config()
        
        # Pass backup file path and current file path to confirm window
        if create_confirm_win_callback:
            create_confirm_win_callback(backup_path, outputs_path, config_dir, profile_name)

    @staticmethod
    def _apply_hyprland_gui(
        display_buttons,
        outputs_activity,
        outputs_path,
        use_desc,
        create_confirm_win_callback,
        config_dir=None,
        profile_name=None,
    ):
        transforms = {
            "normal": 0,
            "90": 1,
            "180": 2,
            "270": 3,
            "flipped": 4,
            "flipped-90": 5,
            "flipped-180": 6,
            "flipped-270": 7,
        }
        lines = [SettingsApplier._get_header()]

        for db in display_buttons:
            name = (
                db.name
                if not use_desc
                else "desc:{}".format(db.description.replace("#", "##"))
            )

            if db.name in outputs_activity and not outputs_activity[db.name]:
                lines.append("monitor={},disable".format(name))
                hyprctl(f"dispatch dpms off {db.name}")
                continue

            # Format: monitor=name,resolution@refresh,position,scale
            line = "monitor={},{}x{}@{},{}x{},{}".format(
                name,
                db.physical_width,
                db.physical_height,
                db.refresh,
                db.x,
                db.y,
                db.scale,
            )
            if db.mirror:
                line += ",mirror,{}".format(db.mirror)
            if db.ten_bit:
                line += ",bitdepth,10"

            lines.append(line)
            if db.transform != "normal":
                lines.append(
                    "monitor={},transform,{}".format(name, transforms[db.transform])
                )

            cmd = "on" if db.dpms else "off"
            hyprctl(f"dispatch dpms {cmd} {db.name}")

        backup = []
        if os.path.isfile(outputs_path):
            backup = load_text_file(outputs_path).splitlines()
        save_list_to_text_file(lines, outputs_path)
        hyprctl("reload")

        if create_confirm_win_callback:
            create_confirm_win_callback(backup, outputs_path, config_dir, profile_name)

    @staticmethod
    def _get_header(source="nwg-displays"):
        now = datetime.datetime.now()
        return "# Generated by {} on {} at {}. Do not edit manually.\n".format(
            source,
            datetime.datetime.strftime(now, "%Y-%m-%d"),
            datetime.datetime.strftime(now, "%H:%M:%S"),
        )

    @staticmethod
    def _save_current_state_to_previous_profile(config_dir):
        """
        Reads the last active profile name, gets current wallpapers,
        and updates that profile's JSON file.
        """
        state_file = os.path.join(config_dir, "active_profile.json")

        if not os.path.isfile(state_file):
            return

        try:
            data = load_json(state_file)
            last_profile_name = data.get("active_profile") if data else None

            if not last_profile_name:
                return

            config, _ = get_config()
            if not config.get("profile-bound-wallpapers", True):
                return

            prev_profile_path = os.path.join(
                config_dir, "profiles", f"{last_profile_name}.json"
            )

            if not os.path.isfile(prev_profile_path):
                print(
                    f"[Warning] Previous profile '{last_profile_name}' file not found. Skipping save."
                )
                return

            current_walls = WallpaperManager.get_current_wallpapers()
            if not current_walls:
                return

            with open(prev_profile_path, "r") as f:
                data = json.load(f)

            if "wallpapers" not in data:
                data["wallpapers"] = {}

            data["wallpapers"].update(current_walls)

            with open(prev_profile_path, "w") as f:
                json.dump(data, f, indent=2)

            print(f"[Persistence] Saved current wallpapers to '{last_profile_name}'")

        except Exception as e:
            print(f"[Error] Failed to save previous state: {e}")

    @staticmethod
    def _set_active_profile(config_dir, profile_name):
        state_file = os.path.join(config_dir, "active_profile.json")
        try:
            save_json({"active_profile": profile_name}, state_file)
        except Exception as e:
            print(f"[Error] Failed to set active profile: {e}")
