"""ManageBac file discovery helpers.

This module is responsible for discovering classes and files from the current
ManageBac student interface.

Design constraints
------------------
- Only the current ``/files`` endpoint is used.
- Only elements matching ``.row.file`` are parsed as file system entries.
- Folder traversal is recursive.
- Pagination is supported.
- Returned file entries are flat and contain relative output paths.

Expected row formats
--------------------
Folder rows:
- ``.row.file``
- title link in ``.title a``
- href contains ``/files/folder/``

File rows:
- ``.row.file``
- file link in ``.details a.title-link``
- author in ``.details label``
- size in ``.size span`` when available
- modified date in right-aligned columns

This module does not download files. It only discovers them.
"""

from __future__ import annotations

import re
from collections.abc import Iterator, Sequence
from dataclasses import dataclass
from http.cookiejar import CookieJar
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup, Tag
from tqdm import tqdm

BASE_HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/120.0.0.0 Safari/537.36"
    ),
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
}

DEFAULT_TIMEOUT = 30
CLASS_MENU_SELECTOR = '#menu > ul > li[data-path^="classes"] > ul > li'
FILE_ROW_SELECTOR = ".row.file"


@dataclass(frozen=True, slots=True)
class ClassInfo:
    """A ManageBac class entry."""

    id: str
    name: str


@dataclass(frozen=True, slots=True)
class FileEntry:
    """A discovered file ready for downstream download handling."""

    name: str
    url: str
    author: str
    date: str
    size: str


@dataclass(frozen=True, slots=True)
class FolderEntry:
    """An internal folder entry used for recursive traversal."""

    name: str
    url: str


def get_classes(school_code: str, cookies: CookieJar) -> list[ClassInfo]:
    """Return the student's classes from the ManageBac dashboard."""
    response = requests.get(
        _absolute_url(school_code, "/student"),
        cookies=cookies,
        headers=BASE_HEADERS,
        allow_redirects=True,
        timeout=DEFAULT_TIMEOUT,
    )
    response.raise_for_status()

    html = BeautifulSoup(response.text, features="lxml")
    classes: list[ClassInfo] = []

    for item in tqdm(
        html.select(CLASS_MENU_SELECTOR), desc="Getting classes...", leave=False
    ):
        class_info = _parse_class_item(item)
        if class_info is not None:
            classes.append(class_info)

    return classes


def get_files(
    school_code: str,
    cookies: CookieJar,
    class_info: ClassInfo,
) -> list[FileEntry]:
    """Return all files for a class, including files in nested folders."""
    root_path = f"/student/classes/{class_info.id}/files"
    with tqdm(
        desc="  Scanning items",
        unit=" item",
        unit_scale=False,
        leave=False,
        bar_format="{desc}: {n} {unit}s found [{elapsed}]",
    ) as progress:
        return _collect_files_recursive(
            school_code=school_code,
            cookies=cookies,
            relative_url=root_path,
            directory="",
            progress=progress,
        )


def _collect_files_recursive(
    school_code: str,
    cookies: CookieJar,
    relative_url: str,
    directory: str,
    progress: tqdm | None = None,
) -> list[FileEntry]:
    """Recursively collect files from the given ManageBac files page."""
    files: list[FileEntry] = []

    for page in _iter_files_pages(school_code, cookies, relative_url):
        if progress is not None:
            progress.update(1)

        for row in page.select(FILE_ROW_SELECTOR):
            parsed = _parse_row(
                school_code=school_code,
                row=row,
                directory=directory,
            )
            if parsed is None:
                continue

            if progress is not None:
                progress.update(1)

            if isinstance(parsed, FolderEntry):
                nested_files = _collect_files_recursive(
                    school_code=school_code,
                    cookies=cookies,
                    relative_url=parsed.url,
                    directory=parsed.name,
                    progress=progress,
                )
                files.extend(nested_files)
            else:
                files.append(parsed)

    return files


def _iter_files_pages(
    school_code: str,
    cookies: CookieJar,
    relative_url: str,
) -> Iterator[BeautifulSoup]:
    """Yield all paginated HTML pages for a given files listing."""
    first_page = _fetch_html(school_code, cookies, relative_url)
    yield first_page

    total_pages = _get_total_pages(first_page)
    for page_number in range(2, total_pages + 1):
        paged_url = f"{relative_url}/page/{page_number}"
        yield _fetch_html(school_code, cookies, paged_url)


def _fetch_html(
    school_code: str,
    cookies: CookieJar,
    relative_url: str,
) -> BeautifulSoup:
    """Fetch a ManageBac HTML page and parse it with BeautifulSoup."""
    response = requests.get(
        _absolute_url(school_code, relative_url),
        cookies=cookies,
        headers=BASE_HEADERS,
        allow_redirects=True,
        timeout=DEFAULT_TIMEOUT,
    )
    response.raise_for_status()
    return BeautifulSoup(response.text, features="lxml")


def _parse_class_item(item: Tag) -> ClassInfo | None:
    """Parse a class menu item into a ClassInfo object."""
    link = item.find("a")
    if link is None:
        return None

    href = _string_attr(link, "href")
    if not href or "/student/classes/" not in href:
        return None

    class_id = href.rstrip("/").split("/")[-1]
    if class_id == "my":
        return None

    span = item.find("span")
    span_text = span.get_text(strip=True) if span is not None else ""
    link_text = link.get_text(" ", strip=True)
    title_text = _string_attr(link, "title")

    class_name = span_text or link_text or title_text
    class_name = _normalize_whitespace(class_name)

    if not class_name or class_name == "Browse All Classes":
        return None

    return ClassInfo(id=class_id, name=class_name)


def _parse_row(
    school_code: str,
    row: Tag,
    directory: str,
) -> FileEntry | FolderEntry | None:
    """Parse a single .row.file element.

    Folder rows are identified by the title link href containing
    ``/files/folder/``.

    File rows are identified by the presence of ``.details a.title-link``.
    """
    title_link = row.select_one(".title a")
    if title_link is None:
        return None

    title_href = _string_attr(title_link, "href")
    if not title_href:
        return None

    if _is_folder_href(title_href):
        folder_name = _clean_link_text(title_link)
        if not folder_name:
            return None

        return FolderEntry(
            name=directory + get_valid_filename(folder_name) + "/",
            url=title_href,
        )

    details_link = row.select_one(".details a.title-link")
    if details_link is None:
        return None

    file_href = _string_attr(details_link, "href")
    file_name = _normalize_whitespace(details_link.get_text(strip=True))

    if not file_href or not file_name:
        return None

    author_label = row.select_one(".details label")
    author = (
        _normalize_author(author_label.get_text("\n", strip=True))
        if author_label is not None
        else "Unknown"
    )

    modified = _extract_modified_text(row)
    size = _extract_size_text(row)

    return FileEntry(
        name=directory + file_name,
        url=_absolute_url(school_code, file_href),
        author=author,
        date=modified,
        size=size,
    )


def _get_total_pages(html: BeautifulSoup) -> int:
    """Return the number of pages in the current paginated listing."""
    page_numbers: list[int] = []

    for link in html.select("ul.pagination li a"):
        text = link.get_text(strip=True)
        if text.isdigit():
            page_numbers.append(int(text))

    return max(page_numbers, default=1)


def _extract_modified_text(row: Tag) -> str:
    """Extract the human-readable modified date text from a row."""
    selectors: Sequence[str] = (
        ".text-right .d-none.d-md-inline",
        ".text-right .d-none.d-md-block",
        ".text-right .d-md-none",
    )

    for selector in selectors:
        node = row.select_one(selector)
        if node is not None:
            return _normalize_whitespace(node.get_text(" ", strip=True))

    return "Unknown"


def _extract_size_text(row: Tag) -> str:
    """Extract the size text from a file row."""
    size_span = row.select_one(".size span")
    if size_span is None:
        return ""
    return _normalize_whitespace(size_span.get_text(strip=True))


def _normalize_author(raw_text: str) -> str:
    """Normalize author label text like 'by Name' to 'Name'."""
    text = _normalize_whitespace(raw_text)
    text = re.sub(r"^by\s+", "", text, flags=re.IGNORECASE)
    return text or "Unknown"


def _clean_link_text(link: Tag) -> str:
    """Return readable text from a title link."""
    return _normalize_whitespace(link.get_text(" ", strip=True))


def _normalize_whitespace(value: str) -> str:
    """Collapse repeated whitespace into a single space."""
    return re.sub(r"\s+", " ", value).strip()


def _string_attr(tag: Tag, name: str) -> str:
    """Safely read an HTML attribute as a string."""
    value = tag.get(name)
    return value.strip() if isinstance(value, str) else ""


def _is_folder_href(href: str) -> bool:
    """Return True if the href points to a ManageBac files folder."""
    return "/files/folder/" in href


def _absolute_url(school_code: str, path_or_url: str) -> str:
    """Convert a ManageBac-relative path into an absolute URL."""
    base_url = f"https://{school_code}.managebac.com"
    return urljoin(base_url, path_or_url)


def get_valid_filename(value: str) -> str:
    """Return a filesystem-safe filename.

    Behavior mirrors Django's ``get_valid_filename``:
    - trim whitespace
    - replace spaces with underscores
    - remove characters not allowed by the regex
    """
    cleaned = str(value).strip().replace(" ", "_")
    return re.sub(r"(?u)[^-\w.]", "", cleaned)
