import re
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from typing import Iterable, Literal, Optional

import httpx
import typer
from bs4 import BeautifulSoup
from rich.console import Console
from rich.table import Table

console = Console()
app = typer.Typer(no_args_is_help=True)

CampusKey = Literal["HSSC", "NSC"]
MealCategory = Literal["L", "B", "D", "S", "R"]  # 중식/조식/석식/간식/예약

# =========================
# 캠퍼스 / 카테고리 정의
# =========================


@dataclass(frozen=True)
class Campus:
    key: CampusKey
    name: str
    list_url: str
    info_url: str


CAMPUSES: dict[CampusKey, Campus] = {
    "HSSC": Campus(
        key="HSSC",
        name="인문사회과학캠퍼스",
        list_url="https://www.skku.edu/skku/campus/support/welfare_11.do",
        info_url="https://www.skku.edu/skku/campus/support/welfare_11.do",
    ),
    "NSC": Campus(
        key="NSC",
        name="자연과학캠퍼스",
        list_url="https://www.skku.edu/skku/campus/support/welfare_11_1.do",
        info_url="https://www.skku.edu/skku/campus/support/welfare_11_1.do",
    ),
}

# CLI 출력용 캠퍼스 이름
CAMPUS_DISPLAY_NAME: dict[CampusKey, str] = {
    "NSC": "자과캠",
    "HSSC": "인문캠",
}

CATEGORY_LABEL: dict[MealCategory, str] = {
    "L": "중식",
    "B": "조식",
    "D": "석식",
    "S": "간식",
    "R": "예약",
}

# =========================
# 데이터 모델
# =========================


@dataclass(frozen=True)
class Restaurant:
    campus: CampusKey
    name: str
    location: str
    conspace_cd: str
    sr_res_id: str
    sr_show_time: Literal["W", "D"]

    def menu_url(self, *, sr_dt: date | str, category: MealCategory = "L") -> str:
        if isinstance(sr_dt, date):
            sr_dt = sr_dt.isoformat()
        base = CAMPUSES[self.campus].info_url
        return (
            f"{base}?mode=info"
            f"&srDt={sr_dt}"
            f"&srCategory={category}"
            f"&conspaceCd={self.conspace_cd}"
            f"&srResId={self.sr_res_id}"
            f"&srShowTime={self.sr_show_time}"
        )


@dataclass(frozen=True)
class MenuEntry:
    day: date
    corner: str
    menu: str
    price: Optional[int] = None
    calories: Optional[int] = None


# =========================
# HTTP / 파싱 유틸
# =========================


def _http_client() -> httpx.Client:
    return httpx.Client(
        timeout=httpx.Timeout(20.0),
        headers={
            "User-Agent": "Mozilla/5.0 (compatible; study-skku-menu/1.0)",
            "Accept-Language": "ko-KR,ko;q=0.9",
        },
        follow_redirects=True,
    )


def _soup_from_url(url: str, *, client: Optional[httpx.Client] = None) -> BeautifulSoup:
    owns = client is None
    if client is None:
        client = _http_client()
    try:
        r = client.get(url)
        r.raise_for_status()
        return BeautifulSoup(r.text, "html.parser")
    finally:
        if owns:
            client.close()


# =========================
# 식당 목록
# =========================


def list_restaurants(
    campus: CampusKey, *, client: Optional[httpx.Client] = None
) -> list[Restaurant]:
    soup = _soup_from_url(CAMPUSES[campus].list_url, client=client)
    out: list[Restaurant] = []

    for li in soup.select("div.wel11_wrap ul.cafeteria_list > li"):
        h5 = li.select_one("h5")
        dd = li.select_one("dl > dd")
        a = li.select_one("a.moreMenu[data-con][data-res][data-show]")
        if not (h5 and dd and a):
            continue

        name = h5.get_text(strip=True)
        location = re.sub(r"\s+", " ", dd.get_text(" ", strip=True))
        con = str(a.get("data-con") or "").strip()
        res = str(a.get("data-res") or "").strip()
        show = str(a.get("data-show") or "").strip().upper()

        if show not in ("W", "D"):
            continue

        out.append(
            Restaurant(
                campus=campus,
                name=name,
                location=location,
                conspace_cd=con,
                sr_res_id=res,
                sr_show_time=show,
            )
        )
    return out


def find_restaurant(restaurants: Iterable[Restaurant], query: str) -> Restaurant:
    q = re.sub(r"\s+", "", query).lower()
    hits = [r for r in restaurants if re.sub(r"\s+", "", r.name).lower() == q]
    if not hits:
        raise ValueError(f"식당을 찾지 못했습니다: {query}")
    if len(hits) > 1:
        raise ValueError("식당명이 중복됩니다. 더 구체적으로 지정하세요.")
    return hits[0]


# =========================
# 메뉴 파싱
# =========================


def _parse_price_int(text: str) -> Optional[int]:
    m = re.search(r"(\d[\d,]*)", text)
    return int(m.group(1).replace(",", "")) if m else None


def _parse_yyyymmdd_dot(text: str) -> date:
    y, m, d = map(int, re.findall(r"\d+", text))
    return date(y, m, d)


def fetch_menus(
    restaurant: Restaurant,
    *,
    sr_dt: date | str,
    category: MealCategory,
    client: Optional[httpx.Client] = None,
) -> dict[date, list[MenuEntry]]:
    soup = _soup_from_url(
        restaurant.menu_url(sr_dt=sr_dt, category=category), client=client
    )
    return (
        _parse_weekly_menu(soup)
        if restaurant.sr_show_time == "W"
        else _parse_oneday_menu(soup)
    )


def fetch_daily_menu(
    restaurant: Restaurant,
    *,
    day: date | str,
    category: MealCategory,
    client: Optional[httpx.Client] = None,
) -> list[MenuEntry]:
    day_obj = datetime.strptime(day, "%Y-%m-%d").date() if isinstance(day, str) else day
    return fetch_menus(restaurant, sr_dt=day_obj, category=category, client=client).get(
        day_obj, []
    )


def _parse_weekly_menu(soup: BeautifulSoup) -> dict[date, list[MenuEntry]]:
    li = soup.select_one("li.oneday_date")
    if li is None:
        raise ValueError("날짜 정보를 찾을 수 없습니다.")
    start, _end = map(_parse_yyyymmdd_dot, li.get_text().split("~"))

    wraps = soup.select("div.weekly_list div.weeListWrap")
    days = [start + timedelta(days=i) for i in range(len(wraps))]

    out: dict[date, list[MenuEntry]] = {d: [] for d in days}
    for d, wrap in zip(days, wraps):
        for cont in wrap.select("div.weeListCont"):
            corner = cont.select_one("h6")
            menu = cont.select_one("pre")
            lis = cont.select("ul > li")

            out[d].append(
                MenuEntry(
                    day=d,
                    corner=corner.get_text(strip=True) if corner else "",
                    menu=menu.get_text("\n", strip=True) if menu else "",
                    price=_parse_price_int(lis[1].text) if len(lis) > 1 else None,
                    calories=_parse_price_int(lis[2].text) if len(lis) > 2 else None,
                )
            )
    return out


def _parse_oneday_menu(soup: BeautifulSoup) -> dict[date, list[MenuEntry]]:
    li = soup.select_one("li.oneday_date")
    if li is None:
        raise ValueError("날짜 정보를 찾을 수 없습니다.")
    d = _parse_yyyymmdd_dot(li.get_text())

    out: dict[date, list[MenuEntry]] = {d: []}
    for box in soup.select("div.corner_box"):
        corner = box.select_one("h5 span")
        menu = box.select_one("pre")
        spans = box.select(".corner_info span")

        price = next(
            (_parse_price_int(s.text) for s in spans if "가격" in s.text), None
        )
        cal = next(
            (_parse_price_int(s.text) for s in spans if "칼로리" in s.text), None
        )

        out[d].append(
            MenuEntry(
                day=d,
                corner=corner.get_text(strip=True) if corner else "",
                menu=menu.get_text("\n", strip=True) if menu else "",
                price=price,
                calories=cal,
            )
        )
    return out


# =========================
# 출력 (Table)
# =========================


def print_menus_table(campus: CampusKey, restaurant: str, menus: list[MenuEntry]):
    info = Table(show_header=False, box=None)
    info.add_row("캠퍼스", CAMPUS_DISPLAY_NAME[campus])
    info.add_row("식당", restaurant)
    console.print(info)

    table = Table(title="메뉴")
    table.add_column("날짜")
    table.add_column("코너")
    table.add_column("메뉴", style="bold")
    table.add_column("가격", justify="right")
    table.add_column("칼로리", justify="right")

    if not menus:
        table.add_row("-", "-", "휴무", "-", "-")
    else:
        for m in menus:
            table.add_row(
                m.day.isoformat(),
                m.corner,
                m.menu,
                "-" if m.price is None else f"{m.price:,}",
                "-" if m.calories is None else str(m.calories),
            )
    console.print(table)


# =========================
# CLI Commands
# =========================


@app.command("list")
def menu_list(
    campus: CampusKey = typer.Option("NSC", "--campus", case_sensitive=False),
    all_campus: bool = typer.Option(False, "--all-campus", help="모든 캠퍼스"),
):
    targets: Iterable[CampusKey] = CAMPUSES.keys() if all_campus else [campus]

    table = Table(title="성균관대 식당 목록")
    table.add_column("캠퍼스", style="cyan")
    table.add_column("식당명", style="bold")
    table.add_column("위치")
    table.add_column("표시(W/D)", justify="center")

    with _http_client() as client:
        for c in targets:
            for r in list_restaurants(c, client=client):
                table.add_row(
                    CAMPUS_DISPLAY_NAME[r.campus],
                    r.name,
                    r.location,
                    r.sr_show_time,
                )
    console.print(table)


@app.command("show")
def menu_show(
    campus: CampusKey = typer.Option("NSC", "--campus", case_sensitive=False),
    restaurant: str = typer.Option("해오름식당", "--restaurant", "-r", help="식당명"),
    day: str = typer.Option(date.today().isoformat(), "--date", "-d", help="날짜"),
    category: MealCategory = typer.Option(
        "L",
        "--category",
        "-c",
        help="시간 (L: 조식, D: 중식, B: 석식, S: 간식, R: 예약)",
        case_sensitive=False,
    ),
):
    with _http_client() as client:
        rs = list_restaurants(campus, client=client)
        r = find_restaurant(rs, restaurant)
        menus = fetch_daily_menu(r, day=day, category=category, client=client)

        print_menus_table(r.campus, r.name, menus)


if __name__ == "__main__":
    app()
