"""HTML parser for Bakalari public timetable."""

import html
import json
import re

import requests
from bs4 import BeautifulSoup

from .models import ChangeType, Day, Lesson, LessonHour, Timetable, TimetableType


class ParseError(Exception):
    """Error parsing timetable data."""
    pass


class FetchError(Exception):
    """Error fetching timetable from URL."""
    pass


def parse_change_type(change_info: str) -> ChangeType:
    """
    Detect change type from Czech change description.
    """
    if not change_info:
        return ChangeType.NONE
    
    info_lower = change_info.lower()
    
    if "odpad" in info_lower or "zruš" in info_lower or "removed" in info_lower:
        return ChangeType.CANCELLED
    elif "změna" in info_lower or "zmena" in info_lower:
        return ChangeType.ROOM_CHANGE
    elif "přesun" in info_lower or "presun" in info_lower:
        return ChangeType.TRANSFERRED
    elif "supluje" in info_lower or "zastupuje" in info_lower:
        return ChangeType.SUBSTITUTE
    elif change_info.strip():
        return ChangeType.OTHER
    
    return ChangeType.NONE


def extract_url_parts(url: str) -> tuple[str, TimetableType, str, str]:
    """Parse Bakalari timetable URL to extract components."""
    url = url.strip()
    if not url.startswith("http"):
        url = "https://" + url
    
    pattern = r"(.+)/Timetable/Public/(Permanent|Actual|Next)/(Class|Teacher|Room)/([^/\s]+)"
    match = re.search(pattern, url, re.IGNORECASE)
    
    if not match:
        raise ParseError(f"Invalid Bakalari timetable URL format: {url}")
    
    base_url = match.group(1)
    type_str = match.group(2)
    entity_type = match.group(3)
    entity_id = match.group(4)
    
    type_map = {
        "permanent": TimetableType.PERMANENT,
        "actual": TimetableType.ACTUAL,
        "next": TimetableType.NEXT,
    }
    timetable_type = type_map.get(type_str.lower(), TimetableType.ACTUAL)
    
    return base_url, timetable_type, entity_type, entity_id


def parse_lesson_hours(soup: BeautifulSoup) -> list[LessonHour]:
    """Parse lesson hour definitions from .bk-timetable-hours."""
    hours: list[LessonHour] = []
    
    hours_container = soup.find("div", class_="bk-timetable-hours")
    if not hours_container:
        return hours
    
    hour_cells = hours_container.find_all("div", class_="bk-hour-wrapper")
    
    for slot, cell in enumerate(hour_cells):
        caption_elem = cell.find("div", class_="num")
        caption = caption_elem.text.strip() if caption_elem else str(slot)
        
        times_elem = cell.find("div", class_="hours")
        start_time = ""
        end_time = ""
        
        if times_elem:
            from_elem = times_elem.find("span", class_="from")
            to_elem = times_elem.find("span", class_="to")
            
            if from_elem:
                start_time = from_elem.text.strip()
            if to_elem:
                end_time = to_elem.text.strip()
        
        hours.append(LessonHour(
            slot=slot,
            caption=caption,
            start_time=start_time,
            end_time=end_time,
        ))
    
    return hours


def parse_lesson_from_cell(cell, slot: int) -> list[Lesson]:
    """Parse lessons from a timetable cell."""
    lessons: list[Lesson] = []
    
    # Check for day-item-volno (day off / free period)
    volno_items = cell.find_all("div", class_="day-item-volno")
    for volno in volno_items:
        text = volno.text.strip() if volno else ""
        lessons.append(Lesson(
            slot=slot,
            is_day_off=True,
            day_off_text=text,
        ))
        return lessons
    
    # Find lesson detail elements (day-item-hover has data-detail JSON)
    hover_nodes = cell.find_all("div", class_="day-item-hover")
    
    for node in hover_nodes:
        detail = node.get("data-detail")
        if not detail:
            continue
        
        try:
            data = json.loads(html.unescape(detail))
        except json.JSONDecodeError:
            continue
        
        # Check for "removed" type
        is_removed = data.get("type") == "removed"
        
        change_info = html.unescape(data.get("changeinfo", ""))
        # For removed type, use removedinfo if available
        if is_removed and not change_info:
            change_info = html.unescape(data.get("removedinfo", "Odpadlo"))
        
        subject_text = html.unescape(data.get("subjecttext", ""))
        
        # Determine change type
        if is_removed:
            change_type = ChangeType.CANCELLED
        else:
            change_type = parse_change_type(change_info)
        
        # Get abbreviation
        subject_short = data.get("subjectabbrev", "")
        
        # If removed and no subject_short, use "X" or "Odpadá"
        if is_removed and not subject_short:
            # Try to guess subject from previous lessons or context? 
            # Difficult without history. Just use a placeholder.
            subject_short = "X"
        
        # If not removed and not in JSON, try to get from day-flex > div.middle
        if not subject_short:
            day_flex = node.find("div", class_="day-flex")
            if day_flex:
                middle = day_flex.find("div", class_="middle")
                if middle:
                    # Get direct text, not text from nested elements
                    middle_text = ""
                    for child in middle.children:
                        if isinstance(child, str):
                            middle_text += child.strip()
                        elif hasattr(child, 'name') and child.name in ('span', 'div'):
                            child_text = child.get_text(strip=True)
                            if child_text and len(child_text) <= 10:
                                middle_text = child_text
                                break
                    
                    if middle_text and len(middle_text) <= 10:
                        subject_short = middle_text
        
        # Final fallback
        if not subject_short:
             # If text contains date (e.g. "Tue 27/1"), avoid using it
            if re.search(r"\d{1,2}[./]\d{1,2}", subject_text):
                 if is_removed:
                     subject_short = "X"
                 else:
                     subject_short = "?"
            elif subject_text:
                if "|" in subject_text:
                    subject_short = subject_text.split("|")[0].strip()[:6]
                else:
                    subject_short = subject_text[:4].upper()
            elif is_removed:
                 subject_short = "X"
        
        lessons.append(Lesson(
            slot=slot,
            subject=subject_text,
            subject_short=subject_short,
            teacher=html.unescape(data.get("teacher", "")),
            room=html.unescape(data.get("room", "")),
            group=html.unescape(data.get("group", "")),
            theme=html.unescape(data.get("theme", "")),
            change_info=change_info,
            change_type=change_type,
        ))
    
    return lessons


def fetch_timetable(url: str, timeout: int = 15) -> Timetable:
    """Fetch and parse timetable from Bakalari URL."""
    base_url, timetable_type, entity_type, entity_id = extract_url_parts(url)
    canonical_url = f"{base_url}/Timetable/Public/{timetable_type.value}/{entity_type}/{entity_id}"
    
    try:
        response = requests.get(canonical_url, timeout=timeout)
        response.raise_for_status()
    except requests.RequestException as e:
        raise FetchError(f"Failed to fetch timetable: {e}")
    
    soup = BeautifulSoup(response.text, "lxml")
    
    hours = parse_lesson_hours(soup)
    
    rows = soup.find_all("div", class_="bk-timetable-row")
    if not rows:
        raise ParseError("No timetable rows found in HTML")
    
    days: list[Day] = []
    
    for row in rows:
        row_classes = row.get("class", [])
        is_day_off = "timetable-day-off" in row_classes
        
        day_elem = row.find("span", class_="bk-day-day")
        date_elem = row.find("span", class_="bk-day-date")
        
        if not day_elem:
            continue
            
        day_name = html.unescape(day_elem.text.strip())
        day_date = date_elem.text.strip() if date_elem else ""
        
        # Check for whole-day off text - look in multiple places
        day_off_text = ""
        
        # Method 1: Check row class and look for w-100 span
        if is_day_off:
            volno_span = row.find("span", class_="w-100")
            if volno_span:
                day_off_text = volno_span.text.strip()
        
        # Method 2: Check for day-item-volno anywhere in the row (this is the case for Friday holidays)
        volno_div = row.find("div", class_="day-item-volno")
        if volno_div:
            is_day_off = True
            # Try to find the text
            day_off_span = volno_div.find("span")
            if day_off_span:
                day_off_text = day_off_span.text.strip()
            elif not day_off_text:
                day_off_text = volno_div.get_text(strip=True)
        
        day = Day(
            name=day_name,
            date=day_date,
            is_day_off=is_day_off,
            day_off_text=day_off_text,
        )
        
        # Only parse cells if NOT a day-off (day-off has special single-cell structure)
        if not is_day_off:
            cells = row.find_all("div", class_="bk-timetable-cell")
            for slot, cell in enumerate(cells):
                lessons = parse_lesson_from_cell(cell, slot)
                day.lessons.extend(lessons)
        
        days.append(day)

    
    return Timetable(
        base_url=base_url,
        timetable_type=timetable_type,
        entity_type=entity_type,
        entity_id=entity_id,
        days=days,
        hours=hours,
    )


def validate_url(url: str) -> tuple[bool, str]:
    """Validate a Bakalari timetable URL."""
    if not url or not url.strip():
        return False, "URL je prázdná"
    
    try:
        extract_url_parts(url)
        return True, ""
    except ParseError as e:
        return False, str(e)
