import os
import random
import re
import urllib.error
import urllib.request

import gi

gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw, Gio, GLib

import stillCenter.AppStore as AppStore
import stillCenter.CategoryPage as CategoryPage
import stillCenter.NetworkErrors as NetworkErrors
import stillCenter.UrlImage as UrlImage

import stillCenter.constants as constants

MAX_HOME_APPS = 8
SCROLL_ANIMATION_DURATION_MS = 180
SCROLL_ANIMATION_INTERVAL_MS = 16

@Gtk.Template(filename=os.path.join(constants.UI_DIR, "FlowAppButton.ui"))
class FlowAppButton(Gtk.Button):
    __gtype_name__ = "FlowAppButton"

    name_label: Gtk.Label = Gtk.Template.Child()
    author_label: Gtk.Label = Gtk.Template.Child()
    icon: UrlImage.UrlImage = Gtk.Template.Child()

    def __init__(self, flow_apps, app_id: str, name: str, author: str, icon: str):
        super().__init__()
        self.flow_apps = flow_apps
        self.app_id = app_id
        self.name_label.set_label(name)
        if author is None:
            self.author_label.set_visible(False)
        else:
            self.author_label.set_visible(True)
            self.author_label.set_label(author)
        self.icon.set_image_url(app_id, icon)
        self.connect("clicked", self.on_click)

    def on_click(self, *args, **kwargs):
        self.flow_apps.stillCenter.app_page.show_app(self.app_id)


@Gtk.Template(filename=os.path.join(constants.UI_DIR, "FlowApps.ui"))
class FlowApps(Gtk.Box):
    __gtype_name__ = "FlowApps"

    label = Gtk.Template.Child()
    flowbox = Gtk.Template.Child()
    show_more_button = Gtk.Template.Child()
    scroller = Gtk.Template.Child()
    next_button = Gtk.Template.Child()
    back_button = Gtk.Template.Child()
    next_revealer = Gtk.Template.Child()
    back_revealer = Gtk.Template.Child()
    stillCenter = None

    def __init__(self):
        super().__init__()
        self._section_title = ""
        self._section_items = []
        self._show_more_busy = False
        self._scroll_update_source_id = 0
        self._scroll_animation_source_id = 0
        self._scroll_animation_start_time = 0
        self._scroll_animation_start_value = 0.0
        self._scroll_animation_target_value = 0.0
        self.show_more_button.connect("clicked", self.on_show_more)
        self.next_button.connect("clicked", self.on_scroll_button_clicked, 1)
        self.back_button.connect("clicked", self.on_scroll_button_clicked, -1)

        h_adjustment = self.scroller.get_hadjustment()
        h_adjustment.connect("changed", self.on_scroll_adjustment_changed)
        h_adjustment.connect("value-changed", self.on_scroll_adjustment_changed)

    def set_section(self, stillCenter, title: str, app_ids: list[str], app_items_by_id: dict):
        self.stillCenter = stillCenter
        self._section_title = title
        self.label.set_label(title)
        self.flowbox.remove_all()
        self._section_items = []

        for app_id in app_ids:
            item = app_items_by_id.get(app_id)
            if item is None:
                continue
            self._section_items.append(item)

        for item in self._section_items[:MAX_HOME_APPS]:
            self.flowbox.append(FlowAppButton(self, item.app_id, item.name, item.author, item.icon))

        self.show_more_button.set_visible(len(self._section_items) > MAX_HOME_APPS)
        self._show_more_busy = False
        self.show_more_button.set_sensitive(True)

        self.set_visible(self.flowbox.get_first_child() is not None)
        self.queue_scroll_button_update()

    def on_show_more(self, _button):
        if self._show_more_busy or not self._section_items:
            return

        self._show_more_busy = True
        self.show_more_button.set_sensitive(False)

        shuffled_items = list(self._section_items)
        random.shuffle(shuffled_items)

        def _restore_show_more():
            try:
                self._show_more_busy = False
                if self.show_more_button.get_visible():
                    self.show_more_button.set_sensitive(True)
            except (RuntimeError, AttributeError):
                pass

        CategoryPage.CategoryPage.push_apps(
            self.stillCenter,
            self._section_title,
            shuffled_items,
            on_page_hidden=_restore_show_more,
        )

    def on_scroll_adjustment_changed(self, _adjustment):
        self.update_scroll_buttons()

    def on_scroll_button_clicked(self, _button: Gtk.Button, direction: int):
        h_adjustment = self.scroller.get_hadjustment()
        max_value = self.get_max_scroll_value()
        step = self.get_scroll_step()
        if max_value <= 0 or step <= 0:
            return

        target = min(max(h_adjustment.get_value() + (step * direction), 0), max_value)
        self.animate_scroll_to(target)

    def get_max_scroll_value(self) -> float:
        h_adjustment = self.scroller.get_hadjustment()
        return max(0.0, h_adjustment.get_upper() - h_adjustment.get_page_size())

    def get_scroll_step(self) -> float:
        flowbox_child = self.flowbox.get_first_child()
        if flowbox_child is None:
            return 0.0

        child_width = flowbox_child.get_allocated_width()
        child = flowbox_child.get_child()
        if child_width <= 0 and child is not None:
            child_width = child.get_allocated_width()
        if child_width <= 0 and child is not None:
            child_width = child.get_width_request()
        if child_width <= 0:
            child_width = 275

        return float(child_width + self.flowbox.get_column_spacing())

    def update_scroll_buttons(self):
        max_value = self.get_max_scroll_value()
        current_value = self.scroller.get_hadjustment().get_value()
        can_scroll = max_value > 1.0

        self.back_revealer.set_reveal_child(can_scroll and current_value > 1.0)
        self.next_revealer.set_reveal_child(can_scroll and current_value < (max_value - 1.0))

    def stop_scroll_animation(self):
        if self._scroll_animation_source_id == 0:
            return

        GLib.source_remove(self._scroll_animation_source_id)
        self._scroll_animation_source_id = 0

    def animate_scroll_to(self, target: float):
        h_adjustment = self.scroller.get_hadjustment()
        current_value = h_adjustment.get_value()
        target = float(target)

        self.stop_scroll_animation()
        if abs(target - current_value) < 1.0:
            h_adjustment.set_value(target)
            return

        self._scroll_animation_start_time = GLib.get_monotonic_time()
        self._scroll_animation_start_value = current_value
        self._scroll_animation_target_value = target

        def _step():
            elapsed_ms = (GLib.get_monotonic_time() - self._scroll_animation_start_time) / 1000.0
            progress = min(1.0, elapsed_ms / SCROLL_ANIMATION_DURATION_MS)
            eased_progress = 1.0 - pow(1.0 - progress, 3)
            new_value = (
                self._scroll_animation_start_value
                + (self._scroll_animation_target_value - self._scroll_animation_start_value) * eased_progress
            )
            h_adjustment.set_value(new_value)

            if progress >= 1.0:
                self._scroll_animation_source_id = 0
                h_adjustment.set_value(self._scroll_animation_target_value)
                return False

            return True

        _step()
        self._scroll_animation_source_id = GLib.timeout_add(SCROLL_ANIMATION_INTERVAL_MS, _step)

    def queue_scroll_button_update(self):
        if self._scroll_update_source_id != 0:
            return

        def _update():
            self._scroll_update_source_id = 0
            self.update_scroll_buttons()
            return GLib.SOURCE_REMOVE

        self._scroll_update_source_id = GLib.idle_add(_update)


def _strip_yaml_value(value: str) -> str:
    value = value.strip()
    if (value.startswith('"') and value.endswith('"')) or (value.startswith("'") and value.endswith("'")):
        return value[1:-1]
    return value


def _normalize_sections(data) -> list[dict]:
    if data is None:
        return []

    if isinstance(data, dict) and "sections" in data:
        data = data["sections"]

    sections = []

    if isinstance(data, list):
        raw_sections = data
    elif isinstance(data, dict):
        # Support map style: { "Section Name": [ids...] }
        raw_sections = [{"name": key, "app_ids": value} for key, value in data.items()]
    else:
        return []

    for section in raw_sections:
        if not isinstance(section, dict):
            continue

        name = section.get("name") or section.get("title")
        ids = (
            section.get("app_ids")
            or section.get("apps")
            or section.get("ids")
            or section.get("sadb_ids")
            or []
        )
        if not isinstance(name, str):
            continue
        if not isinstance(ids, list):
            continue

        cleaned_ids = [str(app_id).strip() for app_id in ids if str(app_id).strip()]
        if cleaned_ids:
            sections.append({"name": name.strip(), "app_ids": cleaned_ids})

    return sections


def _parse_yaml_fallback(text: str) -> list[dict]:
    # Minimal parser for a simple list-of-sections YAML format.
    sections = []
    current = None
    reading_ids = False
    ids_indent = 0

    for raw_line in text.splitlines():
        line = raw_line.split("#", 1)[0].rstrip()
        if not line.strip():
            continue

        indent = len(line) - len(line.lstrip(" "))
        stripped = line.strip()

        if stripped.startswith("- name:"):
            if current and current["app_ids"]:
                sections.append(current)
            current = {"name": _strip_yaml_value(stripped.split(":", 1)[1]), "app_ids": []}
            reading_ids = False
            continue

        if stripped.startswith("name:"):
            if current is None:
                current = {"name": "", "app_ids": []}
            current["name"] = _strip_yaml_value(stripped.split(":", 1)[1])
            reading_ids = False
            continue

        if any(stripped.startswith(key) for key in ("app_ids:", "apps:", "ids:", "sadb_ids:")):
            value = stripped.split(":", 1)[1].strip()
            reading_ids = True
            ids_indent = indent
            if value.startswith("[") and value.endswith("]"):
                inline = [entry.strip() for entry in value[1:-1].split(",")]
                for app_id in inline:
                    app_id = _strip_yaml_value(app_id)
                    if app_id:
                        if current is None:
                            current = {"name": "Featured", "app_ids": []}
                        current["app_ids"].append(app_id)
                reading_ids = False
            continue

        if reading_ids and stripped.startswith("-"):
            app_id = _strip_yaml_value(stripped[1:].strip())
            if app_id:
                if current is None:
                    current = {"name": "Featured", "app_ids": []}
                current["app_ids"].append(app_id)
            continue

        if reading_ids and indent <= ids_indent and ":" in stripped and not stripped.startswith("-"):
            reading_ids = False

    if current and current["app_ids"]:
        sections.append(current)

    return sections


def _load_sections_from_text(yaml_text: str) -> list[dict]:
    try:
        import yaml  # type: ignore
        sections = _normalize_sections(yaml.safe_load(yaml_text))
    except Exception:
        sections = _parse_yaml_fallback(yaml_text)
    return sections


def _load_featured_sections() -> tuple[list[dict], str | None, str | None]:
    url = _get_featured_apps_url()
    remote_error_title: str | None = None
    remote_error_msg: str | None = None

    try:
        with urllib.request.urlopen(url, timeout=8) as response:
            yaml_text = response.read().decode("utf-8")
        if yaml_text:
            sections = _load_sections_from_text(yaml_text)
            if sections:
                return sections, None, None
    except urllib.error.HTTPError as error:
        remote_error_title = "Home page can't be loaded"
        remote_error_msg = f"Unable to load homepage sections from featured repo.\nHTTP Error {error.code}: {error.reason}"
    except (urllib.error.URLError, TimeoutError) as error:
        if NetworkErrors.is_connection_error(error):
            remote_error_title = NetworkErrors.get_connection_error_title(error)
            remote_error_msg = NetworkErrors.build_connection_error_description("the home page", error)
        else:
            remote_error_title = "Home page can't be loaded"
            remote_error_msg = f"Unable to load homepage sections from featured repo.\n{error}"
    except ValueError as error:
        remote_error_title = "Home page can't be loaded"
        remote_error_msg = f"Unable to load homepage sections from featured repo.\n{error}"

    # Fall back to the bundled local featured.yaml
    local_path = constants.FEATURED_APPS_LOCAL
    if os.path.exists(local_path):
        try:
            with open(local_path, "r", encoding="utf-8") as f:
                yaml_text = f.read()
            if yaml_text:
                sections = _load_sections_from_text(yaml_text)
                if sections:
                    return sections, None, None
        except Exception:
            pass

    if remote_error_msg:
        return [], remote_error_title, remote_error_msg
    return [], "Home page can't be loaded", "Homepage featured config is empty or invalid."


def _normalize_app_key(value: str) -> str:
    return re.sub(r"[^a-z0-9]+", "", value.lower())


def _build_app_lookup(app_items_by_id: dict) -> dict[str, AppStore.AppItem]:
    lookup = {}

    for item in app_items_by_id.values():
        variants = set()
        item_id = (item.app_id or "").strip()
        item_name = (item.name or "").strip()

        if item_id:
            variants.add(item_id)
            variants.add(item_id.lower())
            variants.add(_normalize_app_key(item_id))
            for segment in item_id.replace("-", ".").split("."):
                segment = segment.strip().lower()
                if segment:
                    variants.add(segment)
                    variants.add(_normalize_app_key(segment))

        if item_name:
            variants.add(item_name.lower())
            variants.add(_normalize_app_key(item_name))

        for variant in variants:
            if variant and variant not in lookup:
                lookup[variant] = item

    return lookup


def _candidate_lookup_keys(configured_id: str) -> list[str]:
    raw = configured_id.strip()
    keys = []

    def add(value: str):
        if value and value not in keys:
            keys.append(value)

    add(raw)
    add(raw.lower())
    add(_normalize_app_key(raw))

    segments = [segment.strip() for segment in raw.replace("-", ".").split(".") if segment.strip()]
    for index in range(len(segments)):
        tail = ".".join(segments[index:])
        add(tail.lower())
        add(_normalize_app_key(tail))

    for segment in segments:
        lowered = segment.lower()
        add(lowered)
        add(_normalize_app_key(lowered))

    return keys


def _resolve_app_item(app_id: str, app_items_by_id: dict, app_lookup: dict[str, AppStore.AppItem]):
    item = app_items_by_id.get(app_id)
    if item is not None:
        return item

    for key in _candidate_lookup_keys(app_id):
        item = app_lookup.get(key)
        if item is not None:
            return item

    return None


def _build_default_sections() -> list[dict]:
    sections = []
    category_titles = (
        ("Utility", "Utilities"),
        ("Office", "Work and Office"),
        ("Development", "Development"),
        ("Graphics", "Creativity"),
        ("Video", "Media"),
        ("Network", "Browse and Connect"),
    )

    for category, title in category_titles:
        store = AppStore.STORE.get(category)
        if store is None or store.get_n_items() == 0:
            continue

        app_ids = []
        for index in range(min(store.get_n_items(), 4)):
            item = store.get_item(index)
            if item is None or item.app_id in app_ids:
                continue
            app_ids.append(item.app_id)

        if app_ids:
            sections.append({"name": title, "app_ids": app_ids})

    return sections


def _shuffle_app_ids(app_ids: list[str]) -> list[str]:
    shuffled = list(app_ids)
    random.shuffle(shuffled)
    return shuffled


def _get_featured_apps_url() -> str:
    default_url = constants.FEATURED_APPS_URL
    try:
        schema_source = Gio.SettingsSchemaSource.get_default()
        if schema_source is None:
            return default_url

        schema = schema_source.lookup("io.stillhq.stillCenter", True)
        if schema is None:
            return default_url

        settings = Gio.Settings.new_full(schema, None, None)
        configured_url = settings.get_string("featured-apps-url").strip()
        return configured_url or default_url
    except Exception:
        return default_url


def _show_home_error(home_sections: Gtk.Box, stillCenter, title: str, message: str):
    status = Adw.StatusPage()
    status.set_title(title)
    status.set_description(message)
    status.set_icon_name("network-error-symbolic")
    status.set_vexpand(True)
    status.set_hexpand(True)
    button = Gtk.Button(label="Refresh")
    button.set_halign(Gtk.Align.CENTER)
    button.add_css_class("suggested-action")
    button.connect("clicked", lambda _button: populate_apps(home_sections, stillCenter))
    status.set_child(button)
    home_sections.append(status)


def populate_apps(home_sections: Gtk.Box, stillCenter):
    child = home_sections.get_first_child()
    while child is not None:
        next_child = child.get_next_sibling()
        home_sections.remove(child)
        child = next_child

    all_store = AppStore.STORE.get("all")
    if all_store is None:
        _show_home_error(home_sections, stillCenter, "Home page can't be loaded", "App database is unavailable.")
        return

    app_items_by_id = {}
    for i in range(all_store.get_n_items()):
        item = all_store.get_item(i)
        app_items_by_id[item.app_id] = item
    app_lookup = _build_app_lookup(app_items_by_id)

    sections, error_title, error_message = _load_featured_sections()
    if error_message is not None:
        _show_home_error(home_sections, stillCenter, error_title or "Home page can't be loaded", error_message)
        return

    visible_sections = 0
    for section in sections:
        widget = FlowApps()
        resolved_ids = []
        for app_id in section["app_ids"]:
            item = _resolve_app_item(app_id, app_items_by_id, app_lookup)
            if item is None or item.app_id in resolved_ids:
                continue
            resolved_ids.append(item.app_id)
        widget.set_section(stillCenter, section["name"], _shuffle_app_ids(resolved_ids), app_items_by_id)
        if widget.get_visible():
            home_sections.append(widget)
            visible_sections += 1

    if visible_sections > 0:
        return

    for section in _build_default_sections():
        widget = FlowApps()
        widget.set_section(stillCenter, section["name"], _shuffle_app_ids(section["app_ids"]), app_items_by_id)
        if widget.get_visible():
            home_sections.append(widget)
            visible_sections += 1

    if visible_sections == 0:
        _show_home_error(
            home_sections,
            stillCenter,
            "Home page can't be loaded",
            "No featured apps matched the current app catalog."
        )