from __future__ import annotations

import re
import json
from textual import events
from textual.message import Message
from textual.widgets import Static, Button, Input, Select, Label
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.screen import ModalScreen
from textual.app import ComposeResult

class SliderTrack(Static):

    class Dragged(Message):
        def __init__(self, sender: "SliderTrack", ratio: float):
            super().__init__()
            self.sender = sender
            self.ratio = ratio

    def __init__(self, **kwargs):
        super().__init__("", **kwargs)
        self._dragging = False

    def on_mouse_down(self, event: events.MouseDown) -> None:
        self._dragging = True
        self.capture_mouse()
        self._handle_drag(event.x)
        event.stop()

    def on_mouse_move(self, event: events.MouseMove) -> None:
        if self._dragging:
            self._handle_drag(event.x)
            event.stop()

    def on_mouse_up(self, event: events.MouseUp) -> None:
        if self._dragging:
            self._dragging = False
            self.release_mouse()
            event.stop()

    def on_click(self, event: events.Click) -> None:
        self._handle_drag(event.x)
        event.stop()

    def _handle_drag(self, local_x: int) -> None:
        try:
            w = max(1, self.size.width)
            ratio = max(0.0, min(1.0, local_x / w))
            self.post_message(self.Dragged(self, ratio))
        except Exception:
            pass

class NumberSlider(Horizontal):

    class Changed(Message):
        def __init__(self, sender: "NumberSlider", value: float):
            super().__init__()
            self.sender = sender
            self.value = float(value)

    def __init__(
        self,
        value: float = 0,
        min_val: float = 0,
        max_val: float = 100,
        step: float = 1,
        unit: str = "",
        **kwargs
    ):
        super().__init__(**kwargs)
        self._min = float(min_val)
        self._max = float(max_val)
        self._step = float(step)
        self._value = self._clamp(float(value))
        self._unit = unit

    def _clamp(self, v: float) -> float:
        return max(self._min, min(self._max, v))

    def _snap(self, v: float) -> float:
        if self._step > 0:
            v = round(v / self._step) * self._step
        return self._clamp(v)

    @property
    def value(self) -> float:
        return self._value

    def set_value(self, v: float, notify: bool = True) -> None:
        new_val = self._snap(v)
        if new_val != self._value:
            self._value = new_val
            self._update_bar()
            if notify:
                self.post_message(self.Changed(self, self._value))

    def compose(self) -> ComposeResult:
        yield SliderTrack(classes="slider-track", id="slider_track")
        unit_text = f" {self._unit}" if self._unit else ""
        yield Static(f"{int(self._value)}{unit_text}", classes="slider-value", id="slider_value")

    def on_mount(self) -> None:
        self._update_bar()

    def _update_bar(self) -> None:
        try:
            track = self.query_one("#slider_track", SliderTrack)
            value_label = self.query_one("#slider_value", Static)

            pct = (self._value - self._min) / (self._max - self._min) if self._max > self._min else 0
            pct = max(0, min(1, pct))

            try:
                w = max(1, track.size.width)
            except Exception:
                w = 20

            fill = int(pct * w)
            empty = w - fill
            bar = "[bold]" + "#" * fill + "[/][dim]" + "-" * empty + "[/]"
            track.update(bar)

            unit_text = f" {self._unit}" if self._unit else ""
            value_label.update(f"{int(self._value)}{unit_text}")
        except Exception:
            pass

    def on_slider_track_dragged(self, event: SliderTrack.Dragged) -> None:
        new_val = self._min + event.ratio * (self._max - self._min)
        self.set_value(new_val)

def _generate_palette():
    colors = []

    for i in range(64):
        if i < 32:
            v = 255 - i * 8
            colors.append(f"#ff{v:02x}{v:02x}")
        else:
            v = 255 - (i - 32) * 8
            colors.append(f"#{max(v,0):02x}0000")

    for i in range(64):
        if i < 32:
            r = 255
            g = 200 - i * 4
            b = 200 - i * 6
            colors.append(f"#{r:02x}{max(g,100):02x}{max(b,0):02x}")
        else:
            r = 255 - (i - 32) * 6
            g = 100 - (i - 32) * 3
            colors.append(f"#{max(r,50):02x}{max(g,0):02x}00")

    for i in range(64):
        if i < 32:
            r = 255
            g = 255
            b = 200 - i * 6
            colors.append(f"#{r:02x}{g:02x}{max(b,0):02x}")
        else:
            v = 255 - (i - 32) * 6
            colors.append(f"#{max(v,50):02x}{max(v,50):02x}00")

    for i in range(64):
        if i < 32:
            r = 200 - i * 6
            g = 255
            b = 200 - i * 6
            colors.append(f"#{max(r,0):02x}{g:02x}{max(b,0):02x}")
        else:
            r = 0
            g = 255 - (i - 32) * 6
            colors.append(f"#00{max(g,50):02x}00")

    for i in range(64):
        if i < 32:
            r = 200 - i * 6
            g = 255
            b = 200 - i * 6
            colors.append(f"#{max(r,0):02x}{g:02x}{max(b,0):02x}")
        else:
            g = 255 - (i - 32) * 8
            colors.append(f"#00{max(g,0):02x}00")

    for i in range(64):
        if i < 32:
            r = 200 - i * 6
            g = 255
            b = 255
            colors.append(f"#{max(r,0):02x}{g:02x}{b:02x}")
        else:
            g = 255 - (i - 32) * 6
            b = 255 - (i - 32) * 6
            colors.append(f"#00{max(g,50):02x}{max(b,50):02x}")

    for i in range(64):
        if i < 32:
            r = 200 - i * 6
            colors.append(f"#{max(r,0):02x}ffff")
        else:
            v = 255 - (i - 32) * 8
            colors.append(f"#00{max(v,0):02x}{max(v,0):02x}")

    for i in range(64):
        if i < 32:
            r = 200 - i * 6
            g = 220 - i * 4
            colors.append(f"#{max(r,0):02x}{max(g,100):02x}ff")
        else:
            g = 150 - (i - 32) * 4
            b = 255 - (i - 32) * 6
            colors.append(f"#00{max(g,0):02x}{max(b,50):02x}")

    for i in range(64):
        if i < 32:
            v = 200 - i * 6
            colors.append(f"#{max(v,0):02x}{max(v,0):02x}ff")
        else:
            b = 255 - (i - 32) * 8
            colors.append(f"#0000{max(b,0):02x}")

    for i in range(64):
        if i < 32:
            r = 150 - i * 3
            g = 150 - i * 4
            colors.append(f"#{max(r,50):02x}{max(g,0):02x}ff")
        else:
            r = 75 - (i - 32) * 2
            b = 255 - (i - 32) * 6
            colors.append(f"#{max(r,0):02x}00{max(b,50):02x}")

    for i in range(64):
        if i < 32:
            r = 200 - i * 3
            g = 150 - i * 4
            colors.append(f"#{max(r,100):02x}{max(g,0):02x}ff")
        else:
            r = 128 - (i - 32) * 4
            b = 255 - (i - 32) * 6
            colors.append(f"#{max(r,0):02x}00{max(b,50):02x}")

    for i in range(64):
        if i < 32:
            g = 200 - i * 6
            colors.append(f"#ff{max(g,0):02x}ff")
        else:
            v = 255 - (i - 32) * 8
            colors.append(f"#{max(v,0):02x}00{max(v,0):02x}")

    for i in range(64):
        if i < 32:
            g = 200 - i * 5
            b = 220 - i * 4
            colors.append(f"#ff{max(g,50):02x}{max(b,100):02x}")
        else:
            r = 255 - (i - 32) * 6
            b = 150 - (i - 32) * 4
            colors.append(f"#{max(r,50):02x}00{max(b,0):02x}")

    for i in range(64):
        if i < 32:
            r = 240 - i * 3
            g = 220 - i * 5
            b = 180 - i * 5
            colors.append(f"#{max(r,100):02x}{max(g,60):02x}{max(b,20):02x}")
        else:
            r = 140 - (i - 32) * 4
            g = 70 - (i - 32) * 2
            b = 20 - (i - 32)
            colors.append(f"#{max(r,10):02x}{max(g,5):02x}{max(b,0):02x}")

    for i in range(64):
        v = 255 - i * 4
        colors.append(f"#{v:02x}{v:02x}{v:02x}")

    return [(c, "") for c in colors]

PRESET_COLORS = _generate_palette()

class ColorPickerDialog(ModalScreen):

    CSS = """
    ColorPickerDialog { align: center middle; background: transparent !important; }
    #cpd_panel { width: 70; height: auto; max-height: 90%; background: $panel; border: solid $secondary; padding: 1 2; }
    #cpd_title { text-style: bold; margin-bottom: 1; text-align: center; }
    #cpd_scroll { height: auto; max-height: 20; scrollbar-gutter: stable; }
    #cpd_grid { height: auto; width: 100%; }
    .cpd_row { height: 1; width: 100%; }
    .cpd_swatch { width: 4; height: 1; min-width: 4; }
    .cpd_swatch:hover { text-style: bold reverse; }
    .cpd_swatch.selected { text-style: bold underline; }
    #cpd_custom { margin-top: 1; height: 3; }
    #cpd_custom_input { width: 1fr; }
    #cpd_preview { width: 8; margin-left: 1; }
    #cpd_buttons { margin-top: 1; height: 3; }
    #cpd_buttons Button { width: 1fr; margin: 0 1; }
    """

    def __init__(self, current_color: str = "#ffffff"):
        super().__init__()
        self._color = current_color.lower()

    def compose(self) -> ComposeResult:
        from textual.containers import ScrollableContainer
        with Vertical(id="cpd_panel"):
            yield Static("Select Color", id="cpd_title")

            with ScrollableContainer(id="cpd_scroll"):
                with Vertical(id="cpd_grid"):
                    for i in range(0, len(PRESET_COLORS), 16):
                        row_colors = PRESET_COLORS[i:i+16]
                        with Horizontal(classes="cpd_row"):
                            for color, name in row_colors:
                                swatch = Static("████", classes="cpd_swatch")
                                swatch.styles.color = color
                                if name:
                                    swatch.tooltip = name
                                swatch._swatch_color = color
                                if color.lower() == self._color:
                                    swatch.add_class("selected")
                                yield swatch

            with Horizontal(id="cpd_custom"):
                yield Input(self._color, id="cpd_custom_input", placeholder="#rrggbb")
                preview = Static("████", id="cpd_preview")
                preview.styles.color = self._color
                yield preview

            with Horizontal(id="cpd_buttons"):
                yield Button("Cancel", id="cpd_cancel")
                yield Button("OK", id="cpd_ok", variant="primary")

    def on_click(self, event: events.Click) -> None:
        widget = event.widget
        if widget and hasattr(widget, "_swatch_color"):
            self._select_color(widget._swatch_color)
            event.stop()
        elif widget == self:
            self.dismiss(None)

    def _select_color(self, color: str) -> None:
        self._color = color.lower()
        try:
            for swatch in self.query(".cpd_swatch"):
                if hasattr(swatch, "_swatch_color") and swatch._swatch_color.lower() == self._color:
                    swatch.add_class("selected")
                else:
                    swatch.remove_class("selected")
            self.query_one("#cpd_custom_input", Input).value = self._color
            preview = self.query_one("#cpd_preview", Static)
            preview.styles.color = self._color
        except Exception:
            pass

    def on_input_changed(self, event: Input.Changed) -> None:
        if event.input.id == "cpd_custom_input":
            v = event.value.strip()
            if not v.startswith("#"):
                v = "#" + v
            if re.match(r'^#[0-9a-fA-F]{6}$', v):
                self._color = v.lower()
                try:
                    preview = self.query_one("#cpd_preview", Static)
                    preview.styles.color = self._color
                    for swatch in self.query(".cpd_swatch"):
                        if hasattr(swatch, "_swatch_color") and swatch._swatch_color.lower() == self._color:
                            swatch.add_class("selected")
                        else:
                            swatch.remove_class("selected")
                except Exception:
                    pass

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "cpd_cancel":
            self.dismiss(None)
        elif event.button.id == "cpd_ok":
            self.dismiss(self._color)

class ColorPicker(Horizontal):

    class Changed(Message):
        def __init__(self, sender: "ColorPicker", color: str):
            super().__init__()
            self.sender = sender
            self.color = color

    def __init__(self, value: str = "#ffffff", **kwargs):
        super().__init__(**kwargs)
        self._color = value.lower()

    @property
    def color(self) -> str:
        return self._color

    def set_color(self, c: str, notify: bool = True) -> None:
        if re.match(r'^#[0-9a-fA-F]{6}$', c):
            self._color = c.lower()
            self._update_display()
            if notify:
                self.post_message(self.Changed(self, self._color))

    def compose(self) -> ComposeResult:
        yield Static("███", id="cp_preview")
        yield Static(self._color, id="cp_hex")
        yield Button("...", id="cp_btn", classes="cp_open_btn")

    def on_mount(self) -> None:
        self._update_display()

    def _update_display(self) -> None:
        try:
            preview = self.query_one("#cp_preview", Static)
            preview.styles.color = self._color
            hex_label = self.query_one("#cp_hex", Static)
            hex_label.update(self._color)
        except Exception:
            pass

    async def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "cp_btn":
            result = await self.app.push_screen_wait(ColorPickerDialog(self._color))
            if result:
                self.set_color(result)
            event.stop()

class ColorRule:

    RULE_TYPES = [

        ("user", "Specific User"),
        ("regex_name", "Username Regex"),
        ("user_contains", "Username Contains"),

        ("permission", "Has Permission"),
        ("is_self", "Is Me (Self)"),

        ("regex_msg", "Message Regex"),
        ("msg_contains", "Message Contains"),

        ("is_reply", "Is Reply"),
        ("is_edited", "Is Edited"),
        ("has_file", "Has Attachment"),
        ("has_image", "Has Image"),

        ("time_range", "Time Range (HH:MM-HH:MM)"),
        ("date_regex", "Date/Time Regex"),

        ("chat_dm", "Is Direct Message"),
        ("chat_group", "Is Group Chat"),

        ("custom_python", "Custom Python Code"),
    ]

    PERMISSIONS = [
        ("kick", "Can Kick"),
        ("ban", "Can Ban"),
        ("admin", "Is Admin/Creator"),
        ("moderator", "Is Moderator"),
        ("online", "Is Online"),
        ("in_call", "In Voice Call"),
    ]

    PREVIOUS_COLOR = "__PREVIOUS__"

    def __init__(
        self,
        rule_type: str = "user",
        pattern: str = "",
        color: str = "#ffffff",
        color2: str | None = None,
        split: bool = False,
        enabled: bool = True,
        priority: int = 0,
    ):
        self.rule_type = rule_type
        self.pattern = pattern
        self.color = color
        self.color2 = color2
        self.split = split
        self.enabled = enabled
        self.priority = priority

    def to_dict(self) -> dict:
        return {
            "rule_type": self.rule_type,
            "pattern": self.pattern,
            "color": self.color,
            "color2": self.color2,
            "split": self.split,
            "enabled": self.enabled,
            "priority": self.priority,
        }

    @classmethod
    def from_dict(cls, d: dict) -> "ColorRule":
        return cls(
            rule_type=d.get("rule_type", "user"),
            pattern=d.get("pattern", ""),
            color=d.get("color", "#ffffff"),
            color2=d.get("color2"),
            split=d.get("split", False),
            enabled=d.get("enabled", True),
            priority=d.get("priority", 0),
        )

    def matches(self, alias: str, message: str = "", permissions: set[str] | None = None, is_self: bool = False, msg_meta: dict | None = None, user_info: dict | None = None) -> bool:
        from datetime import datetime

        if not self.enabled:
            return False

        perms = permissions or set()
        meta = msg_meta or {}
        user = user_info or {}
        msg_lower = message.lower()
        alias_lower = alias.lower()
        pattern_lower = self.pattern.lower() if self.pattern else ""
        now = datetime.now()

        if self.rule_type == "user":
            return alias_lower == pattern_lower

        if self.rule_type == "regex_name":
            try:
                return bool(re.search(self.pattern, alias, re.IGNORECASE))
            except Exception:
                return False

        if self.rule_type == "user_contains":
            return pattern_lower in alias_lower

        if self.rule_type == "permission":
            return pattern_lower in perms

        if self.rule_type == "is_self":
            return is_self

        if self.rule_type == "regex_msg":
            try:
                return bool(re.search(self.pattern, message, re.IGNORECASE))
            except Exception:
                return False

        if self.rule_type == "msg_contains":
            return pattern_lower in msg_lower

        if self.rule_type == "is_reply":
            return bool(meta.get("reply_to") or meta.get("reply_id"))

        if self.rule_type == "is_edited":
            return bool(meta.get("edited") or meta.get("edit_date"))

        if self.rule_type == "has_file":
            return bool(meta.get("filename") or meta.get("file_id"))

        if self.rule_type == "has_image":
            mime = str(meta.get("mime", ""))
            return mime.startswith("image/")

        if self.rule_type == "time_range":

            try:
                parts = self.pattern.split("-")
                if len(parts) == 2:
                    start_h, start_m = map(int, parts[0].strip().split(":"))
                    end_h, end_m = map(int, parts[1].strip().split(":"))
                    current_mins = now.hour * 60 + now.minute
                    start_mins = start_h * 60 + start_m
                    end_mins = end_h * 60 + end_m

                    if start_mins <= end_mins:
                        return start_mins <= current_mins <= end_mins
                    else:

                        return current_mins >= start_mins or current_mins <= end_mins
            except:
                pass
            return False

        if self.rule_type == "date_regex":

            try:
                date_str = now.strftime("%Y-%m-%d %H:%M:%S")
                day_name = now.strftime("%A")
                time_str = now.strftime("%H:%M")
                combined = f"{date_str} {day_name} {time_str}"
                return bool(re.search(self.pattern, combined, re.IGNORECASE))
            except:
                return False

        if self.rule_type == "chat_dm":
            return meta.get("chat_type") == "dm" or meta.get("is_dm")

        if self.rule_type == "chat_group":
            return meta.get("chat_type") == "group" or meta.get("is_group")

        if self.rule_type == "custom_python":
            return self._eval_custom_python(alias, message, perms, is_self, meta, user, now)

        return False

    def _eval_custom_python(self, alias: str, message: str, perms: set, is_self: bool, meta: dict, user: dict, now) -> bool:
        try:

            context = {

                "alias": alias,
                "user_id": user.get("user_id", ""),
                "fg_color": user.get("fg_color", ""),
                "status": user.get("status", ""),
                "bio": user.get("bio", ""),
                "created_at": user.get("created_at", ""),
                "joined_at": user.get("joined_at", ""),
                "avatar": user.get("avatar", ""),
                "role": user.get("role", ""),
                "is_self": is_self,
                "perms": perms,

                "message": message,
                "msg_len": len(message),
                "word_count": len(message.split()) if message else 0,
                "meta": meta,
                "is_reply": bool(meta.get("reply_to")),
                "is_edited": bool(meta.get("edited")),
                "has_file": bool(meta.get("filename")),
                "filename": meta.get("filename", ""),
                "mime_type": meta.get("mime", ""),

                "has_url": bool(re.search(r'https?://\S+', message)),
                "has_emoji": any(ord(c) > 0x1F300 for c in message),
                "is_caps": message.isupper() and len(message) > 3,
                "has_mention": "@" in message,
                "is_question": message.rstrip().endswith("?"),

                "date": now.strftime("%Y-%m-%d"),
                "time": now.strftime("%H:%M:%S"),
                "datetime": now.strftime("%Y-%m-%d %H:%M:%S"),
                "weekday": now.strftime("%A"),
                "hour": now.hour,
                "minute": now.minute,
                "second": now.second,
                "day": now.day,
                "month": now.month,
                "year": now.year,

                "re": re,
                "len": len,
                "int": int,
                "str": str,
                "bool": bool,
                "abs": abs,
                "min": min,
                "max": max,
                "sum": sum,
                "any": any,
                "all": all,
                "True": True,
                "False": False,
            }

            result = eval(self.pattern, {"__builtins__": {}}, context)
            return bool(result)
        except Exception as e:

            return False

class ColorRuleItem(Static):

    class EditRequested(Message):
        def __init__(self, sender: "ColorRuleItem", index: int):
            super().__init__()
            self.sender = sender
            self.index = index

    class DeleteRequested(Message):
        def __init__(self, sender: "ColorRuleItem", index: int):
            super().__init__()
            self.sender = sender
            self.index = index

    class ToggleRequested(Message):
        def __init__(self, sender: "ColorRuleItem", index: int):
            super().__init__()
            self.sender = sender
            self.index = index

    class MoveUpRequested(Message):
        def __init__(self, sender: "ColorRuleItem", index: int):
            super().__init__()
            self.sender = sender
            self.index = index

    class MoveDownRequested(Message):
        def __init__(self, sender: "ColorRuleItem", index: int):
            super().__init__()
            self.sender = sender
            self.index = index

    DEFAULT_CSS = """
    ColorRuleItem {
        height: 2;
        width: 100%;
        padding: 0 1;
        background: $panel;
        margin-bottom: 1;
    }
    ColorRuleItem:hover {
        background: $panel-lighten-1;
    }
    ColorRuleItem.disabled {
        color: $text-muted;
    }
    ColorRuleItem .rule-color {
        width: 3;
        height: 1;
    }
    ColorRuleItem .rule-color2 {
        width: 3;
        height: 1;
    }
    ColorRuleItem .rule-type {
        width: 12;
        color: $text-muted;
    }
    ColorRuleItem .rule-pattern {
        width: 1fr;
    }
    ColorRuleItem .rule-btn {
        width: 3;
        height: 1;
        min-width: 3;
        border: none;
        background: transparent;
    }
    ColorRuleItem .rule-btn-move {
        width: 2;
        height: 1;
        min-width: 2;
        border: none;
        background: transparent;
        color: $text-muted;
    }
    ColorRuleItem .rule-btn-move:hover {
        color: $accent;
    }
    """

    def __init__(self, rule: ColorRule, index: int, total: int = 1, **kwargs):
        super().__init__(**kwargs)
        self.rule = rule
        self.index = index
        self.total = total
        if not rule.enabled:
            self.add_class("disabled")

    def compose(self) -> ComposeResult:
        type_labels = dict(ColorRule.RULE_TYPES)
        type_label = type_labels.get(self.rule.rule_type, self.rule.rule_type)

        with Horizontal():

            yield Button("▲", classes="rule-btn-move", id="btn_up", disabled=(self.index == 0))
            yield Button("▼", classes="rule-btn-move", id="btn_down", disabled=(self.index >= self.total - 1))

            yield Static("██", classes="rule-color", id="rule_color1")
            if self.rule.split and self.rule.color2:
                yield Static("██", classes="rule-color2", id="rule_color2")
            yield Static(f"[{type_label}]", classes="rule-type")
            yield Static(self.rule.pattern[:20] or "(all)", classes="rule-pattern")
            yield Button("✓" if self.rule.enabled else "○", classes="rule-btn", id="btn_toggle")
            yield Button("✎", classes="rule-btn", id="btn_edit")
            yield Button("×", classes="rule-btn", id="btn_delete")

    def on_mount(self) -> None:
        try:
            c1 = self.query_one("#rule_color1", Static)
            c1.styles.color = self.rule.color
        except Exception:
            pass
        try:
            c2 = self.query_one("#rule_color2", Static)
            c2.styles.color = self.rule.color2 or self.rule.color
        except Exception:
            pass

    def on_button_pressed(self, event: Button.Pressed) -> None:
        bid = event.button.id
        if bid == "btn_toggle":
            self.post_message(self.ToggleRequested(self, self.index))
        elif bid == "btn_edit":
            self.post_message(self.EditRequested(self, self.index))
        elif bid == "btn_delete":
            self.post_message(self.DeleteRequested(self, self.index))
        elif bid == "btn_up":
            self.post_message(self.MoveUpRequested(self, self.index))
        elif bid == "btn_down":
            self.post_message(self.MoveDownRequested(self, self.index))
        event.stop()

class ColorRulesManager:

    def __init__(self, db=None):
        self.db = db
        self._rules: list[ColorRule] = []
        self._my_color: str = "#ffffff"
        self._default_color: str = "#cccccc"
        self._load()

    def _load(self) -> None:
        if not self.db:
            return
        try:
            raw = self.db.get_setting("appearance_color_rules")
            if raw:
                data = json.loads(raw)
                self._rules = [ColorRule.from_dict(r) for r in data.get("rules", [])]
                self._my_color = data.get("my_color", "#ffffff")
                self._default_color = data.get("default_color", "#cccccc")
        except Exception:
            self._rules = []
            self._my_color = "#ffffff"
            self._default_color = "#cccccc"

    def _save(self) -> None:
        if not self.db:
            return
        try:
            data = {
                "rules": [r.to_dict() for r in self._rules],
                "my_color": self._my_color,
                "default_color": self._default_color,
            }
            self.db.set_setting("appearance_color_rules", json.dumps(data))
        except Exception:
            pass

    @property
    def rules(self) -> list[ColorRule]:
        return list(self._rules)

    @property
    def my_color(self) -> str:
        return self._my_color

    @my_color.setter
    def my_color(self, c: str) -> None:
        self._my_color = c
        self._save()

    @property
    def default_color(self) -> str:
        return self._default_color

    @default_color.setter
    def default_color(self, c: str) -> None:
        self._default_color = c
        self._save()

    def add_rule(self, rule: ColorRule) -> None:
        self._rules.append(rule)
        self._save()

    def update_rule(self, index: int, rule: ColorRule) -> None:
        if 0 <= index < len(self._rules):
            self._rules[index] = rule
            self._save()

    def delete_rule(self, index: int) -> None:
        if 0 <= index < len(self._rules):
            del self._rules[index]
            self._save()

    def toggle_rule(self, index: int) -> None:
        if 0 <= index < len(self._rules):
            self._rules[index].enabled = not self._rules[index].enabled
            self._save()

    def move_rule_up(self, index: int) -> None:
        if 0 < index < len(self._rules):
            self._rules[index], self._rules[index - 1] = self._rules[index - 1], self._rules[index]
            self._save()

    def move_rule_down(self, index: int) -> None:
        if 0 <= index < len(self._rules) - 1:
            self._rules[index], self._rules[index + 1] = self._rules[index + 1], self._rules[index]
            self._save()

    def _check_rule_match(
        self,
        alias: str,
        message: str = "",
        permissions: set[str] | None = None,
        is_self: bool = False,
        msg_meta: dict | None = None
    ) -> ColorRule | None:
        for rule in self._rules:
            if rule.matches(alias, message, permissions, is_self=is_self, msg_meta=msg_meta):
                return rule
        return None

    def get_color_for_user(
        self,
        alias: str,
        message: str = "",
        permissions: set[str] | None = None,
        is_self: bool = False,
        default: str | None = None,
        msg_meta: dict | None = None
    ) -> tuple[str, str | None]:

        if default is None:
            default = self._default_color

        if is_self:
            previous_color = self._my_color
        else:
            previous_color = default

        rule = self._check_rule_match(alias, message, permissions, is_self, msg_meta)
        if rule:

            color1 = previous_color if rule.color == ColorRule.PREVIOUS_COLOR else rule.color
            color2 = None
            if rule.split and rule.color2:
                color2 = previous_color if rule.color2 == ColorRule.PREVIOUS_COLOR else rule.color2
            return (color1, color2)

        return (previous_color, None)

    def get_color_for_message_group(
        self,
        messages: list[dict],
        alias: str,
        is_self: bool = False,
        permissions: set[str] | None = None
    ) -> tuple[str, str | None]:
        default = self._default_color
        base_color = self._my_color if is_self else default

        for msg in reversed(messages):
            body = msg.get("body") or ""
            meta = msg.get("meta") or {}

            rule = self._check_rule_match(alias, body, permissions, is_self, meta)
            if rule:

                color1 = base_color if rule.color == ColorRule.PREVIOUS_COLOR else rule.color
                color2 = None
                if rule.split and rule.color2:
                    color2 = base_color if rule.color2 == ColorRule.PREVIOUS_COLOR else rule.color2
                return (color1, color2)

        return (base_color, None)

class AppearanceCategory(Vertical):

    DEFAULT_CSS = """
    AppearanceCategory {
        height: auto;
        width: 100%;
        max-width: 100%;
        margin-bottom: 1;
        border: solid $secondary;
        background: $panel;
        padding: 1;
        overflow: hidden;
    }
    AppearanceCategory .cat-header {
        height: 1;
        width: 100%;
        content-align: center middle;
        text-style: bold;
        color: $text;
        margin-bottom: 1;
    }
    AppearanceCategory .cat-content {
        padding: 0 1;
        height: auto;
        width: 100%;
        max-width: 100%;
        background: $panel;
        overflow: hidden;
    }
    """

    def __init__(self, title: str, collapsed: bool = False, **kwargs):
        super().__init__(**kwargs)
        self._title = title

    def compose(self) -> ComposeResult:
        yield Static(self._title, classes="cat-header")
        yield Vertical(classes="cat-content", id="cat_content")

    def get_content(self) -> Vertical:
        return self.query_one("#cat_content", Vertical)
