import json
import re
from urllib.parse import quote, urljoin, urlparse, urlunparse

from bs4 import BeautifulSoup

from .decoder import base64_decode_text
from .models import Category, ResolvedTrack, Track

RANK_LINKS = {
    "\u65e5\u699c": "index-0-5.htm",
    "\u5468\u699c": "index-0-4.htm",
    "\u6708\u699c": "index-0-3.htm",
}

FORUM_NAMES = {
    "\u534e\u8bed",
    "\u6b27\u7f8e",
    "\u65e5\u97e9",
    "Remix",
    "\u7eaf\u97f3\u4e50",
}


def extract_categories(html: str, base_url: str) -> list[Category]:
    soup = BeautifulSoup(html, "html.parser")
    categories: list[Category] = []
    seen: set[tuple[str, str]] = set()

    for name in ("\u65e5\u699c", "\u5468\u699c", "\u6708\u699c"):
        href = RANK_LINKS[name]
        categories.append(Category(name=name, url=urljoin(base_url, href), kind="rank"))
        seen.add((name, "rank"))

    for link in soup.select('a[href^="forum-"]'):
        name = link.get_text(" ", strip=True)
        if not name or name not in FORUM_NAMES:
            alt = link.select_one("img[alt]")
            name = (alt.get("alt", "") if alt else "").replace("\u56fe\u6807", "").strip()
        href = link.get("href", "")
        key = (name, "forum")
        if name in FORUM_NAMES and href and key not in seen:
            categories.append(Category(name=name, url=urljoin(base_url, href), kind="forum"))
            seen.add(key)

    return categories


def infer_rank_page_links(url: str, base_url: str) -> tuple[str | None, str | None]:
    match = re.search(r"(?:^|/)index-(\d+)-([345])\.htm$", urlparse(url).path)
    if not match:
        return None, None
    page = int(match.group(1))
    rank = match.group(2)
    previous_url = urljoin(base_url, f"index-{page - 1}-{rank}.htm") if page > 0 else None
    next_url = urljoin(base_url, f"index-{page + 1}-{rank}.htm")
    return previous_url, next_url


def extract_tracks(html: str, base_url: str) -> list[Track]:
    soup = BeautifulSoup(html, "html.parser")
    tracks: list[Track] = []
    seen: set[str] = set()

    for link in soup.select('a[href^="thread-"]'):
        href = link.get("href", "")
        title = link.get_text(" ", strip=True)
        if not href or not title or "#comments" in href:
            continue
        if not re.match(r"^thread-\d+\.htm$", href):
            continue
        url = urljoin(base_url, href)
        if url in seen:
            continue
        seen.add(url)
        container = link.find_parent(attrs={"data-tid": True})
        category_link = container.select_one('a[href^="forum-"]') if container else None
        category = category_link.get_text(" ", strip=True) if category_link else None
        tracks.append(Track(title=title, url=url, category=category))

    return tracks


def extract_page_links(html: str, base_url: str) -> tuple[str | None, str | None]:
    soup = BeautifulSoup(html, "html.parser")
    pagination = soup.select_one(".pagination")
    if not pagination:
        return None, None

    active = pagination.select_one(".page-item.active .page-link")
    active_page = int(active.get_text(strip=True)) if active and active.get_text(strip=True).isdigit() else None
    numeric_pages: dict[int, str] = {}
    previous_url: str | None = None
    next_url: str | None = None

    for link in pagination.select("a.page-link[href]"):
        text = link.get_text(strip=True)
        href = link.get("href", "")
        if not href:
            continue
        absolute = urljoin(base_url, href)
        if text.isdigit():
            numeric_pages[int(text)] = absolute
        elif text in {"<", "&lt;", "\u2190", "\u25c0", "\u4e0a\u4e00\u9875"}:
            previous_url = absolute
        elif text in {">", "&gt;", "\u2192", "\u25b6", "\u4e0b\u4e00\u9875"}:
            next_url = absolute

    if active_page is not None:
        previous_url = previous_url or numeric_pages.get(active_page - 1)
        next_url = next_url or numeric_pages.get(active_page + 1)
    return previous_url, next_url


def extract_lyrics(html: str) -> str | None:
    soup = BeautifulSoup(html, "html.parser")
    heading = soup.find(lambda tag: tag.name in {"h4", "h5"} and "\u6b4c\u8bcd" in tag.get_text(" ", strip=True))
    if not heading:
        return None
    lines: list[str] = []
    for sibling in heading.find_next_siblings():
        if sibling.name in {"h4", "h5"}:
            break
        text = sibling.get_text(" ", strip=True)
        if text:
            lines.append(text)
    return "\n".join(lines) if lines else None


def extract_album(html: str) -> str | None:
    soup = BeautifulSoup(html, "html.parser")
    for label in soup.find_all(string=re.compile("\u4e13\u8f91")):
        parent = label.parent
        if parent:
            text = parent.get_text(" ", strip=True)
            match = re.search(r"\u4e13\u8f91[:?]\s*(.+)", text)
            if match:
                return match.group(1).strip()
    return None


def _quote_url_path(url: str) -> str:
    parsed = urlparse(url)
    return urlunparse(parsed._replace(path=quote(parsed.path)))


def _title_artist_from_page(html: str) -> tuple[str, str]:
    soup = BeautifulSoup(html, "html.parser")
    heading = soup.select_one("h1")
    raw_title = heading.get_text(" ", strip=True) if heading else ""
    if not raw_title and soup.title:
        raw_title = soup.title.get_text(" ", strip=True).split("\u4e0b\u8f7d", 1)[0]
    if " - " in raw_title:
        artist, title = raw_title.split(" - ", 1)
        return title.strip(), artist.strip()
    return raw_title.strip(), ""


def extract_legacy_aplayer_track(html: str, thread_url: str) -> ResolvedTrack:
    match = re.search(r"url\s*:\s*base64_decode\([\"']([^\"']+)[\"']\)", html)
    if not match:
        raise ValueError("legacy APlayer url not found")

    decoded_url = base64_decode_text(match.group(1))
    absolute_url = _quote_url_path(urljoin(thread_url, decoded_url))

    pic_match = re.search(r"pic\s*:\s*[\"']([^\"']+)[\"']", html)
    cover = urljoin(thread_url, pic_match.group(1)) if pic_match else None
    title, artist = _title_artist_from_page(html)
    return ResolvedTrack(title=title, artist=artist, url=absolute_url, cover=cover, album=extract_album(html), lyrics=extract_lyrics(html))


def extract_music_tags(html: str) -> list[dict[str, str]]:
    match = re.search(r"const\s+musicTags\s*=\s*(\[[\s\S]*?\])\s*;", html)
    if not match:
        raise ValueError("musicTags not found")
    return json.loads(match.group(1))
