"""
Scraper / parser for ktunotes.live subject data.

Dual-mode: scrapes subject.ktunotes.live via HTTP (works on Termux,
remote machines, etc.) and falls back to local JS data files if
available (for S3/S4 which are JS-rendered).
"""

import re
import os
from pathlib import Path
from typing import Dict, Optional

import requests
from bs4 import BeautifulSoup

from ktunotes.ui import show_info, show_error, show_success

# ─── Constants ───────────────────────────────────────────────────────────────
SUBJECT_BASE = "https://subject.ktunotes.live"
SEMESTER_URLS = {
    "S1": f"{SUBJECT_BASE}/semesters/s1",
    "S2": f"{SUBJECT_BASE}/semesters/s2",
    "S3": f"{SUBJECT_BASE}/semesters/s3",
    "S4": f"{SUBJECT_BASE}/semesters/s4",
}
REQUEST_TIMEOUT = 15
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (X11; Linux x86_64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) "
        "Chrome/120 Safari/537.36"
    ),
}

# Subject code pattern — e.g. GAMAT101, PCCST402, GBEST213
_CODE_RE = re.compile(r'\b([A-Z]{2,6}\d{2,4})\b')

# Google Drive folder URL pattern
_GDRIVE_RE = re.compile(
    r'https://drive\.google\.com/drive/folders/[A-Za-z0-9_-]+'
)

# Local JS data directory (relative to project root — only exists on dev machines)
_DEFAULT_DATA_DIR = os.path.join(
    os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
    "ktunotes1", "ktunotes-next", "app", "semesters"
)
_LOCAL_DATA_FILES = {
    "S1": "s1/data.js",
    "S2": "s2/data.js",
    "S3": "s3/data.js",
    "S4": "s4/s4Data.js",
}

# Debug output directory
_DEBUG_DIR = Path.home() / ".ktunotes_cache"


# ─── HTTP Scraping ───────────────────────────────────────────────────────────

def _fetch_page(url: str) -> Optional[str]:
    """
    Fetch a page with proper headers. Returns HTML string or None.
    """
    try:
        resp = requests.get(url, headers=HEADERS, timeout=REQUEST_TIMEOUT)
        if resp.status_code != 200:
            show_error(f"HTTP {resp.status_code} from {url}")
            return None
        html = resp.text
        if len(html) < 500:
            show_error(f"Page too short ({len(html)} chars): {url}")
            return None
        return html
    except requests.exceptions.ConnectionError:
        show_error(f"Connection failed: {url}")
        return None
    except requests.exceptions.Timeout:
        show_error(f"Request timed out: {url}")
        return None
    except requests.exceptions.RequestException as e:
        show_error(f"Request error: {e}")
        return None


def _save_debug_html(html: str, filename: str = "debug_page.html") -> None:
    """Save fetched HTML for debugging."""
    try:
        _DEBUG_DIR.mkdir(parents=True, exist_ok=True)
        path = _DEBUG_DIR / filename
        path.write_text(html, encoding="utf-8")
    except IOError:
        pass


def _extract_codes_from_semester_page(html: str) -> list[tuple[str, str]]:
    """
    Extract (code, title) pairs from a semester listing page.
    Looks for links like subject.ktunotes.live/GAMAT101.
    """
    soup = BeautifulSoup(html, "html.parser")
    results = []
    seen = set()

    # Find all links to subject.ktunotes.live/<CODE>
    for link in soup.find_all("a", href=True):
        href = link["href"]
        # Match links like https://subject.ktunotes.live/GAMAT101
        # or just /GAMAT101
        match = re.search(r'subject\.ktunotes\.live/([A-Z]{2,6}\d{2,4})$', href)
        if not match:
            match = re.search(r'^/([A-Z]{2,6}\d{2,4})$', href)
        if match:
            code = match.group(1)
            if code not in seen:
                seen.add(code)
                # Try to get title from the nearest heading
                title = link.get_text(strip=True) or code
                results.append((code, title))

    # Also find codes mentioned in text but not as links
    for heading in soup.find_all(["h2", "h3", "h4"]):
        text = heading.get_text(strip=True)
        for m in _CODE_RE.finditer(text):
            code = m.group(1)
            if code not in seen:
                seen.add(code)
                # Clean title — remove the code from the heading text
                title = _CODE_RE.sub("", text).strip(" -–—()")
                results.append((code, title or code))

    return results


def _scrape_subject_page(code: str) -> Optional[dict]:
    """
    Scrape an individual subject page to get name + gdrive link.
    URL: subject.ktunotes.live/<CODE>
    """
    url = f"{SUBJECT_BASE}/{code}"
    html = _fetch_page(url)
    if not html:
        return None

    soup = BeautifulSoup(html, "html.parser")

    # Extract name from the first h1 or og:description
    name = ""
    h1 = soup.find("h1")
    if h1:
        name = h1.get_text(strip=True)
        # Remove the code if embedded in the name
        name = name.replace(code, "").strip(" -–—()")

    if not name:
        og = soup.find("meta", property="og:title")
        if og and og.get("content"):
            name = og["content"]

    # Extract description
    desc = ""
    og_desc = soup.find("meta", property="og:description")
    if og_desc and og_desc.get("content"):
        desc = og_desc["content"]

    # Extract Google Drive folder link — look for "Modules" link
    gdrive = ""
    for link in soup.find_all("a", href=True):
        href = link["href"]
        text = link.get_text(strip=True).lower()
        if "drive.google.com/drive/folders" in href:
            # Prefer Modules link over others
            if "module" in text or "lab manual" in text or not gdrive:
                gdrive = href.split("?")[0]  # Remove query params

    return {
        "name": name or code,
        "gdrive": gdrive,
        "description": desc,
    }


def scrape_via_http() -> Dict[str, dict]:
    """
    Scrape subjects from subject.ktunotes.live via HTTP.

    Strategy:
    1. Fetch each semester listing page to discover subject codes
    2. For each code, fetch the individual subject page to get Drive link
    """
    all_subjects = {}

    for semester, url in SEMESTER_URLS.items():
        html = _fetch_page(url)
        if not html:
            continue

        codes = _extract_codes_from_semester_page(html)

        if not codes:
            # Save debug HTML for inspection
            _save_debug_html(html, f"debug_{semester.lower()}.html")
            show_info(
                f"{semester}: Page fetched but no subjects parsed. "
                f"Debug HTML saved."
            )
            continue

        show_info(f"{semester}: Found {len(codes)} subject codes")

        for code, title in codes:
            code_lower = code.lower()
            if code_lower in all_subjects:
                continue

            # Fetch individual subject page for the Drive link
            subject_data = _scrape_subject_page(code)
            if subject_data:
                all_subjects[code_lower] = {
                    "name": subject_data["name"] or title,
                    "semester": semester,
                    "branch": "Common" if semester in ("S1", "S2") else "All",
                    "gdrive": subject_data["gdrive"],
                    "description": subject_data.get("description", ""),
                }
            else:
                # Store with code only — no Drive link
                all_subjects[code_lower] = {
                    "name": title,
                    "semester": semester,
                    "branch": "Common" if semester in ("S1", "S2") else "All",
                    "gdrive": "",
                    "description": "",
                }

    return all_subjects


# ─── Local JS File Parsing (fallback for dev machines) ──────────────────────

_BRANCHES = ["cse", "it", "ece", "eee", "mech", "civil", "chemical"]


def _find_subject_objects(text: str) -> list[str]:
    """Extract subject objects from JS by matching balanced braces."""
    objects = []
    i = 0
    while i < len(text):
        brace_pos = text.find('{', i)
        if brace_pos == -1:
            break
        depth = 1
        j = brace_pos + 1
        while j < len(text) and depth > 0:
            if text[j] == '{':
                depth += 1
            elif text[j] == '}':
                depth -= 1
            j += 1
        block = text[brace_pos:j]
        if 'title' in block and 'buttons' in block and 'href' in block:
            objects.append(block)
        i = brace_pos + 1
    return objects


def _extract_field(block: str, field: str) -> str:
    """Extract a string field from a JS object."""
    match = re.search(rf'{field}\s*:\s*["\'](.+?)["\']', block, re.DOTALL)
    return match.group(1).strip() if match else ""


def _extract_gdrive_link(block: str) -> str:
    """Find the Google Drive folder link from Modules/Lab Manual buttons."""
    module_pattern = re.compile(
        r'label\s*:\s*["\'](?:Modules|Lab Manual)["\']'
        r'.*?href\s*:\s*["\']'
        r'(https://drive\.google\.com/drive/folders/[^"\']+)'
        r'["\']',
        re.DOTALL,
    )
    match = module_pattern.search(block)
    if match:
        return match.group(1)
    fallback = re.search(
        r'href\s*:\s*["\']'
        r'(https://drive\.google\.com/drive/folders/[^"\']+)'
        r'["\']',
        block,
    )
    return fallback.group(1) if fallback else ""


def _detect_branch(block: str, full_text: str, semester: str) -> str:
    """Detect the branch context for a subject in S3/S4 JS data."""
    if semester in ("S1", "S2"):
        return "Common"
    pos = full_text.find(block[:80])
    if pos == -1:
        return "All"
    preceding = full_text[:pos]
    branch_matches = list(re.finditer(
        r'\b(' + '|'.join(_BRANCHES) + r')\s*[:{]',
        preceding, re.IGNORECASE,
    ))
    return branch_matches[-1].group(1).upper() if branch_matches else "All"


def parse_local_data_file(filepath: str, semester: str) -> Dict[str, dict]:
    """Parse a single JS data file and return subjects keyed by code."""
    subjects = {}
    try:
        with open(filepath, "r", encoding="utf-8") as f:
            content = f.read()
    except (FileNotFoundError, IOError):
        return subjects

    objects = _find_subject_objects(content)
    for block in objects:
        title = _extract_field(block, "title")
        if not title or len(title) < 3:
            continue
        if any(s in title.upper() for s in ["PREVIOUS YEAR", "NASSCOM", "DIGITAL 101"]):
            continue

        code = _extract_field(block, "code").lower()
        if not code:
            m = re.search(r'\(([A-Z]{2,6}\d{2,4})\)', title)
            code = m.group(1).lower() if m else ""
        if not code:
            slug = re.sub(r'[^a-z0-9]+', '_', title.lower()).strip('_')
            code = slug[:25]

        gdrive = _extract_gdrive_link(block)
        description = _extract_field(block, "description")
        branch = _detect_branch(block, content, semester)
        clean = re.sub(r'\s*\([A-Z]{2,6}\d{2,4}\)\s*', '', title).strip()

        subjects[code] = {
            "name": clean or title,
            "semester": semester,
            "branch": branch,
            "gdrive": gdrive,
            "description": description,
        }
    return subjects


def scrape_local_files(data_dir: Optional[str] = None) -> Dict[str, dict]:
    """Parse all local JS data files."""
    if data_dir is None:
        data_dir = _DEFAULT_DATA_DIR
    all_subjects = {}
    for semester, rel_path in _LOCAL_DATA_FILES.items():
        filepath = os.path.join(data_dir, rel_path)
        if os.path.exists(filepath):
            for code, data in parse_local_data_file(filepath, semester).items():
                if code not in all_subjects:
                    all_subjects[code] = data
    return all_subjects


# ─── Public API ──────────────────────────────────────────────────────────────

def scrape_all(data_dir: Optional[str] = None) -> Dict[str, dict]:
    """
    Scrape subjects using both HTTP and local files.

    Strategy:
    1. Try HTTP scraping (works on Termux and any machine with internet)
    2. Supplement with local JS file parsing (adds S3/S4 branch subjects
       that aren't available via the semester listing pages)
    3. Merge results — HTTP data takes precedence for overlapping codes
    """
    all_subjects = {}

    # 1. Scrape via HTTP first (works everywhere)
    show_info("Fetching from subject.ktunotes.live...")
    http_subjects = scrape_via_http()
    all_subjects.update(http_subjects)

    # 2. Supplement with local JS files (adds S3/S4 data on dev machines)
    if data_dir is None:
        data_dir = _DEFAULT_DATA_DIR
    if os.path.isdir(data_dir):
        show_info("Supplementing with local data files...")
        local_subjects = scrape_local_files(data_dir)
        for code, data in local_subjects.items():
            if code not in all_subjects:
                all_subjects[code] = data

    # 3. Debug output if nothing was found
    if not all_subjects:
        show_error(
            "Page fetched successfully but no subjects were parsed. "
            "HTML structure may have changed."
        )
        show_info(
            "Debug HTML saved to ~/.ktunotes_cache/debug_*.html"
        )

    return all_subjects
