"""CalDAV Bridge — Synology Calendar REST API 래퍼 (port 8092)"""

import logging
import os
import uuid
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

import httpx
from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from icalendar import Calendar, Event, Todo, vText

load_dotenv()

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("caldav_bridge")

CALDAV_BASE_URL = os.getenv("CALDAV_BASE_URL", "https://192.168.1.227:5001/caldav")
CALDAV_USER = os.getenv("CALDAV_USER", "chatbot-api")
CALDAV_PASSWORD = os.getenv("CALDAV_PASSWORD", "")
CALDAV_CALENDAR = os.getenv("CALDAV_CALENDAR", "chatbot")
KST = ZoneInfo("Asia/Seoul")

CALENDAR_URL = f"{CALDAV_BASE_URL}/{CALDAV_USER}/{CALDAV_CALENDAR}/"

app = FastAPI()


def _client() -> httpx.AsyncClient:
    return httpx.AsyncClient(
        verify=False,
        auth=(CALDAV_USER, CALDAV_PASSWORD),
        timeout=15,
    )


def _make_ical(title: str, start: str, end: str | None, location: str | None,
               description: str | None, uid: str) -> bytes:
    """iCalendar 이벤트 생성."""
    cal = Calendar()
    cal.add("prodid", "-//SynChatBot//CalDAV Bridge//KO")
    cal.add("version", "2.0")
    cal.add("calscale", "GREGORIAN")

    # VTIMEZONE for Asia/Seoul (required by Synology Calendar)
    from icalendar import Timezone, TimezoneStandard
    tz = Timezone()
    tz.add("tzid", "Asia/Seoul")
    tz_std = TimezoneStandard()
    tz_std.add("tzname", "KST")
    tz_std.add("tzoffsetfrom", timedelta(hours=9))
    tz_std.add("tzoffsetto", timedelta(hours=9))
    tz_std.add("dtstart", datetime(1970, 1, 1))
    tz.add_component(tz_std)
    cal.add_component(tz)

    evt = Event()
    evt.add("uid", uid)
    evt.add("dtstamp", datetime.now(KST))
    evt.add("summary", title)

    dt_start = datetime.fromisoformat(start)
    if dt_start.tzinfo is None:
        dt_start = dt_start.replace(tzinfo=KST)
    evt.add("dtstart", dt_start)

    if end:
        dt_end = datetime.fromisoformat(end)
        if dt_end.tzinfo is None:
            dt_end = dt_end.replace(tzinfo=KST)
        evt.add("dtend", dt_end)
    else:
        evt.add("dtend", dt_start + timedelta(hours=1))

    if location:
        evt["location"] = vText(location)
    if description:
        evt["description"] = vText(description)

    cal.add_component(evt)
    return cal.to_ical()


def _parse_events(ical_data: bytes) -> list[dict]:
    """iCalendar 데이터에서 이벤트 목록 추출."""
    events = []
    try:
        cals = Calendar.from_ical(ical_data, multiple=True) if hasattr(Calendar, 'from_ical') else [Calendar.from_ical(ical_data)]
    except Exception:
        cals = [Calendar.from_ical(ical_data)]
    for cal in cals:
        for component in cal.walk():
            if component.name == "VEVENT":
                dt_start = component.get("dtstart")
                dt_end = component.get("dtend")
                events.append({
                    "uid": str(component.get("uid", "")),
                    "title": str(component.get("summary", "")),
                    "start": dt_start.dt.isoformat() if dt_start else None,
                    "end": dt_end.dt.isoformat() if dt_end else None,
                    "location": str(component.get("location", "")),
                    "description": str(component.get("description", "")),
                })
    return events


def _make_vtodo(title: str, due: str | None, description: str | None, uid: str) -> bytes:
    """iCalendar VTODO 생성."""
    cal = Calendar()
    cal.add("prodid", "-//SynChatBot//CalDAV Bridge//KO")
    cal.add("version", "2.0")
    cal.add("calscale", "GREGORIAN")

    from icalendar import Timezone, TimezoneStandard
    tz = Timezone()
    tz.add("tzid", "Asia/Seoul")
    tz_std = TimezoneStandard()
    tz_std.add("tzname", "KST")
    tz_std.add("tzoffsetfrom", timedelta(hours=9))
    tz_std.add("tzoffsetto", timedelta(hours=9))
    tz_std.add("dtstart", datetime(1970, 1, 1))
    tz.add_component(tz_std)
    cal.add_component(tz)

    todo = Todo()
    todo.add("uid", uid)
    todo.add("dtstamp", datetime.now(KST))
    todo.add("summary", title)
    todo.add("created", datetime.now(KST))
    todo.add("status", "NEEDS-ACTION")

    if due:
        dt_due = datetime.fromisoformat(due)
        if dt_due.tzinfo is None:
            dt_due = dt_due.replace(tzinfo=KST)
        todo.add("due", dt_due)

    if description:
        todo["description"] = vText(description)

    cal.add_component(todo)
    return cal.to_ical()


@app.post("/calendar/create")
async def create_event(request: Request):
    body = await request.json()
    title = body.get("title", "")
    start = body.get("start", "")
    end = body.get("end")
    location = body.get("location")
    description = body.get("description")

    if not title or not start:
        return JSONResponse({"success": False, "error": "title and start required"}, status_code=400)

    uid = f"{uuid.uuid4()}@syn-chat-bot"
    ical = _make_ical(title, start, end, location, description, uid)

    # 표시용 시간 포맷
    dt = datetime.fromisoformat(start)
    display_time = dt.strftime("%-m/%d %H:%M")

    async with _client() as client:
        resp = await client.put(
            f"{CALENDAR_URL}{uid}.ics",
            content=ical,
            headers={"Content-Type": "text/calendar; charset=utf-8"},
        )
        if resp.status_code in (200, 201, 204):
            logger.info(f"Event created: {uid} '{title}'")
            return JSONResponse({"success": True, "uid": uid,
                                 "message": f"일정 등록 완료: {display_time} {title}"})
        logger.error(f"CalDAV PUT failed: {resp.status_code} {resp.text[:200]}")
        return JSONResponse({"success": False, "error": f"CalDAV PUT {resp.status_code}",
                             "message": f"일정 등록 실패: {title}"}, status_code=502)


@app.post("/calendar/create-todo")
async def create_todo(request: Request):
    """VTODO 생성. body: {title, due?, description?}"""
    body = await request.json()
    title = body.get("title", "")
    due = body.get("due")
    description = body.get("description")

    if not title:
        return JSONResponse({"success": False, "error": "title required"}, status_code=400)

    uid = f"{uuid.uuid4()}@syn-chat-bot"
    ical = _make_vtodo(title, due, description, uid)

    # 표시용 기한
    display_due = ""
    if due:
        dt = datetime.fromisoformat(due)
        display_due = f"~{dt.strftime('%-m/%d')} "

    async with _client() as client:
        resp = await client.put(
            f"{CALENDAR_URL}{uid}.ics",
            content=ical,
            headers={"Content-Type": "text/calendar; charset=utf-8"},
        )
        if resp.status_code in (200, 201, 204):
            logger.info(f"Todo created: {uid} '{title}'")
            return JSONResponse({"success": True, "uid": uid,
                                 "message": f"작업 등록 완료: {display_due}{title}"})
        logger.error(f"CalDAV PUT (todo) failed: {resp.status_code} {resp.text[:200]}")
        return JSONResponse({"success": False, "error": f"CalDAV PUT {resp.status_code}",
                             "message": f"작업 등록 실패: {title}"}, status_code=502)


@app.post("/calendar/query")
async def query_events(request: Request):
    body = await request.json()
    start = body.get("start", "")
    end = body.get("end", "")

    if not start or not end:
        return JSONResponse({"success": False, "error": "start and end required"}, status_code=400)

    dt_start = datetime.fromisoformat(start)
    dt_end = datetime.fromisoformat(end)
    if dt_start.tzinfo is None:
        dt_start = dt_start.replace(tzinfo=KST)
    if dt_end.tzinfo is None:
        dt_end = dt_end.replace(tzinfo=KST)

    xml_body = f"""<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
  <D:prop>
    <D:getetag/>
    <C:calendar-data/>
  </D:prop>
  <C:filter>
    <C:comp-filter name="VCALENDAR">
      <C:comp-filter name="VEVENT">
        <C:time-range start="{dt_start.strftime('%Y%m%dT%H%M%SZ')}"
                      end="{dt_end.strftime('%Y%m%dT%H%M%SZ')}"/>
      </C:comp-filter>
    </C:comp-filter>
  </C:filter>
</C:calendar-query>"""

    async with _client() as client:
        resp = await client.request(
            "REPORT",
            CALENDAR_URL,
            content=xml_body.encode("utf-8"),
            headers={
                "Content-Type": "application/xml; charset=utf-8",
                "Depth": "1",
            },
        )
        if resp.status_code not in (200, 207):
            logger.error(f"CalDAV REPORT failed: {resp.status_code}")
            return JSONResponse({"success": False, "error": f"CalDAV REPORT {resp.status_code}"}, status_code=502)

        # Parse multistatus XML for calendar-data
        events = []
        import xml.etree.ElementTree as ET
        try:
            root = ET.fromstring(resp.text)
            ns = {"D": "DAV:", "C": "urn:ietf:params:xml:ns:caldav"}
            for response_elem in root.findall(".//D:response", ns):
                cal_data_elem = response_elem.find(".//C:calendar-data", ns)
                if cal_data_elem is not None and cal_data_elem.text:
                    events.extend(_parse_events(cal_data_elem.text.encode("utf-8")))
        except ET.ParseError as e:
            logger.error(f"XML parse error: {e}")

        return JSONResponse({"success": True, "events": events})


@app.post("/calendar/update")
async def update_event(request: Request):
    body = await request.json()
    uid = body.get("uid", "")
    if not uid:
        return JSONResponse({"success": False, "error": "uid required"}, status_code=400)

    ics_url = f"{CALENDAR_URL}{uid}.ics"

    async with _client() as client:
        # Fetch existing event
        resp = await client.get(ics_url)
        if resp.status_code != 200:
            return JSONResponse({"success": False, "error": f"Event not found: {resp.status_code}"}, status_code=404)

        # Parse and modify
        try:
            cal = Calendar.from_ical(resp.content)
        except Exception as e:
            return JSONResponse({"success": False, "error": f"Parse error: {e}"}, status_code=500)

        for component in cal.walk():
            if component.name == "VEVENT":
                if "title" in body and body["title"]:
                    component["summary"] = vText(body["title"])
                if "start" in body and body["start"]:
                    dt = datetime.fromisoformat(body["start"])
                    if dt.tzinfo is None:
                        dt = dt.replace(tzinfo=KST)
                    component["dtstart"].dt = dt
                if "end" in body and body["end"]:
                    dt = datetime.fromisoformat(body["end"])
                    if dt.tzinfo is None:
                        dt = dt.replace(tzinfo=KST)
                    component["dtend"].dt = dt
                if "location" in body:
                    component["location"] = vText(body["location"]) if body["location"] else ""
                if "description" in body:
                    component["description"] = vText(body["description"]) if body["description"] else ""
                break

        # PUT back
        resp = await client.put(
            ics_url,
            content=cal.to_ical(),
            headers={"Content-Type": "text/calendar; charset=utf-8"},
        )
        if resp.status_code in (200, 201, 204):
            logger.info(f"Event updated: {uid}")
            return JSONResponse({"success": True, "uid": uid})
        return JSONResponse({"success": False, "error": f"CalDAV PUT {resp.status_code}"}, status_code=502)


@app.post("/calendar/delete")
async def delete_event(request: Request):
    body = await request.json()
    uid = body.get("uid", "")
    if not uid:
        return JSONResponse({"success": False, "error": "uid required"}, status_code=400)

    async with _client() as client:
        resp = await client.delete(f"{CALENDAR_URL}{uid}.ics")
        if resp.status_code in (200, 204):
            logger.info(f"Event deleted: {uid}")
            return JSONResponse({"success": True})
        return JSONResponse({"success": False, "error": f"CalDAV DELETE {resp.status_code}"}, status_code=502)


@app.get("/health")
async def health():
    caldav_reachable = False
    try:
        async with _client() as client:
            resp = await client.request(
                "PROPFIND",
                CALENDAR_URL,
                headers={"Depth": "0", "Content-Type": "application/xml"},
                content=b'<?xml version="1.0"?><D:propfind xmlns:D="DAV:"><D:prop><D:displayname/></D:prop></D:propfind>',
            )
            caldav_reachable = resp.status_code in (200, 207)
    except Exception as e:
        logger.warning(f"CalDAV health check failed: {e}")

    return {"status": "ok", "caldav_reachable": caldav_reachable}
