"""
Nextcloud WebDAV client.

All paths passed to these functions are relative to `settings.nextcloud_base_path`.
The full WebDAV URL is:
  {nextcloud_url}/remote.php/dav/files/{username}/{base_path}/{relative_path}
"""
import xml.etree.ElementTree as ET
from typing import List
from urllib.parse import unquote

import httpx
from fastapi import HTTPException

from config import settings

DAV_NS = "DAV:"

# Default timeout for all Nextcloud WebDAV requests (seconds)
_TIMEOUT = 60.0

# Shared async client — reuses TCP connections across requests so Nextcloud
# doesn't see rapid connection bursts that trigger brute-force throttling.
_http_client: httpx.AsyncClient | None = None


def _get_client() -> httpx.AsyncClient:
    global _http_client
    if _http_client is None or _http_client.is_closed:
        _http_client = httpx.AsyncClient(
            timeout=_TIMEOUT,
            follow_redirects=True,
            headers={"User-Agent": "BellSystems-CP/1.0"},
        )
    return _http_client


async def close_client() -> None:
    """Close the shared HTTP client. Call this on application shutdown."""
    global _http_client
    if _http_client and not _http_client.is_closed:
        await _http_client.aclose()
    _http_client = None


async def keepalive_ping() -> None:
    """
    Send a lightweight PROPFIND Depth:0 to the Nextcloud base folder to keep
    the TCP connection alive. Safe to call even if Nextcloud is not configured.
    """
    if not settings.nextcloud_url:
        return
    try:
        url = _base_url()
        client = _get_client()
        await client.request(
            "PROPFIND",
            url,
            auth=_auth(),
            headers={"Depth": "0", "Content-Type": "application/xml"},
            content=_PROPFIND_BODY,
        )
    except Exception as e:
        print(f"[NEXTCLOUD KEEPALIVE] ping failed: {e}")


def _dav_user() -> str:
    """The username used in the WebDAV URL path (may differ from the login username)."""
    return settings.nextcloud_dav_user or settings.nextcloud_username


def _base_url() -> str:
    if not settings.nextcloud_url:
        raise HTTPException(status_code=503, detail="Nextcloud not configured")
    return (
        f"{settings.nextcloud_url.rstrip('/')}"
        f"/remote.php/dav/files/{_dav_user()}"
        f"/{settings.nextcloud_base_path}"
    )


def _auth() -> tuple[str, str]:
    return (settings.nextcloud_username, settings.nextcloud_password)


def _full_url(relative_path: str) -> str:
    """Build full WebDAV URL for a relative path."""
    path = relative_path.strip("/")
    base = _base_url()
    return f"{base}/{path}" if path else base


def _parse_propfind(xml_bytes: bytes, base_path_prefix: str) -> List[dict]:
    """
    Parse a PROPFIND XML response.
    Returns list of file/folder entries, skipping the root itself.
    """
    root = ET.fromstring(xml_bytes)
    results = []

    # The prefix we need to strip from D:href to get the relative path back
    # href looks like: /remote.php/dav/files/user/BellSystems/Console/customers/abc/
    dav_prefix = (
        f"/remote.php/dav/files/{_dav_user()}"
        f"/{settings.nextcloud_base_path}/"
    )

    for response in root.findall(f"{{{DAV_NS}}}response"):
        href_el = response.find(f"{{{DAV_NS}}}href")
        if href_el is None:
            continue
        href = unquote(href_el.text or "")

        # Strip DAV prefix to get relative path within base_path
        if href.startswith(dav_prefix):
            rel = href[len(dav_prefix):].rstrip("/")
        else:
            rel = href

        # Skip the folder itself (the root of the PROPFIND request)
        if rel == base_path_prefix.strip("/"):
            continue

        propstat = response.find(f"{{{DAV_NS}}}propstat")
        if propstat is None:
            continue
        prop = propstat.find(f"{{{DAV_NS}}}prop")
        if prop is None:
            continue

        # is_dir: resourcetype contains D:collection
        resource_type = prop.find(f"{{{DAV_NS}}}resourcetype")
        is_dir = resource_type is not None and resource_type.find(f"{{{DAV_NS}}}collection") is not None

        content_type_el = prop.find(f"{{{DAV_NS}}}getcontenttype")
        mime_type = content_type_el.text if content_type_el is not None else (
            "inode/directory" if is_dir else "application/octet-stream"
        )

        size_el = prop.find(f"{{{DAV_NS}}}getcontentlength")
        size = int(size_el.text) if size_el is not None and size_el.text else 0

        modified_el = prop.find(f"{{{DAV_NS}}}getlastmodified")
        last_modified = modified_el.text if modified_el is not None else None

        filename = rel.split("/")[-1] if rel else ""

        results.append({
            "filename": filename,
            "path": rel,
            "mime_type": mime_type,
            "size": size,
            "last_modified": last_modified,
            "is_dir": is_dir,
        })

    return results


async def ensure_folder(relative_path: str) -> None:
    """
    Create a folder (and all parents) in Nextcloud via MKCOL.
    Includes the base_path segments so the full hierarchy is created from scratch.
    Silently succeeds if folders already exist.
    """
    # Build the complete path list: base_path segments + relative_path segments
    base_parts = settings.nextcloud_base_path.strip("/").split("/")
    rel_parts = relative_path.strip("/").split("/") if relative_path.strip("/") else []
    all_parts = base_parts + rel_parts

    dav_root = f"{settings.nextcloud_url.rstrip('/')}/remote.php/dav/files/{_dav_user()}"
    client = _get_client()
    built = ""
    for part in all_parts:
        built = f"{built}/{part}" if built else part
        url = f"{dav_root}/{built}"
        resp = await client.request("MKCOL", url, auth=_auth())
        # 201 = created, 405/409 = already exists — both are fine
        if resp.status_code not in (201, 405, 409):
            raise HTTPException(
                status_code=502,
                detail=f"Failed to create Nextcloud folder '{built}': {resp.status_code}",
            )


async def write_info_file(customer_folder: str, customer_name: str, customer_id: str) -> None:
    """Write a _info.txt stub into a new customer folder for human browsability."""
    content = f"Customer: {customer_name}\nID: {customer_id}\n"
    await upload_file(
        f"{customer_folder}/_info.txt",
        content.encode("utf-8"),
        "text/plain",
    )


_PROPFIND_BODY = b"""<?xml version="1.0"?>
<D:propfind xmlns:D="DAV:">
  <D:prop>
    <D:resourcetype/>
    <D:getcontenttype/>
    <D:getcontentlength/>
    <D:getlastmodified/>
  </D:prop>
</D:propfind>"""


async def list_folder(relative_path: str) -> List[dict]:
    """
    PROPFIND at depth=1 to list a folder's immediate children.
    relative_path is relative to nextcloud_base_path.
    """
    url = _full_url(relative_path)
    client = _get_client()
    resp = await client.request(
        "PROPFIND",
        url,
        auth=_auth(),
        headers={"Depth": "1", "Content-Type": "application/xml"},
        content=_PROPFIND_BODY,
    )
    if resp.status_code == 404:
        return []
    if resp.status_code not in (207, 200):
        raise HTTPException(status_code=502, detail=f"Nextcloud PROPFIND failed: {resp.status_code}")
    return _parse_propfind(resp.content, relative_path)


async def list_folder_recursive(relative_path: str) -> List[dict]:
    """
    Recursively list ALL files under a folder (any depth).
    Tries Depth:infinity first (single call). Falls back to manual recursion
    via Depth:1 if the server returns 403/400 (some servers disable infinity).
    Returns only file entries (is_dir=False).
    """
    url = _full_url(relative_path)
    client = _get_client()
    resp = await client.request(
        "PROPFIND",
        url,
        auth=_auth(),
        headers={"Depth": "infinity", "Content-Type": "application/xml"},
        content=_PROPFIND_BODY,
    )

    if resp.status_code in (207, 200):
        all_items = _parse_propfind(resp.content, relative_path)
        return [item for item in all_items if not item["is_dir"]]

    # Depth:infinity not supported — fall back to recursive Depth:1
    if resp.status_code in (403, 400, 412):
        return await _list_recursive_fallback(relative_path)

    if resp.status_code == 404:
        return []

    raise HTTPException(status_code=502, detail=f"Nextcloud PROPFIND failed: {resp.status_code}")


async def _list_recursive_fallback(relative_path: str) -> List[dict]:
    """Manually recurse via Depth:1 calls when Depth:infinity is blocked."""
    items = await list_folder(relative_path)
    files = []
    dirs = []
    for item in items:
        if item["is_dir"]:
            dirs.append(item["path"])
        else:
            files.append(item)
    for dir_path in dirs:
        child_files = await _list_recursive_fallback(dir_path)
        files.extend(child_files)
    return files


async def upload_file(relative_path: str, content: bytes, mime_type: str) -> str:
    """
    PUT a file to Nextcloud. Returns the relative_path on success.
    relative_path includes filename, e.g. "customers/abc123/media/photo.jpg"
    """
    url = _full_url(relative_path)
    client = _get_client()
    resp = await client.put(
        url,
        auth=_auth(),
        content=content,
        headers={"Content-Type": mime_type},
    )
    if resp.status_code not in (200, 201, 204):
        raise HTTPException(status_code=502, detail=f"Nextcloud upload failed: {resp.status_code}")
    return relative_path


async def download_file(relative_path: str) -> tuple[bytes, str]:
    """
    GET a file from Nextcloud. Returns (bytes, mime_type).
    """
    url = _full_url(relative_path)
    client = _get_client()
    resp = await client.get(url, auth=_auth())
    if resp.status_code == 404:
        raise HTTPException(status_code=404, detail="File not found in Nextcloud")
    if resp.status_code != 200:
        raise HTTPException(status_code=502, detail=f"Nextcloud download failed: {resp.status_code}")
    mime = resp.headers.get("content-type", "application/octet-stream").split(";")[0].strip()
    return resp.content, mime


async def delete_file(relative_path: str) -> None:
    """DELETE a file from Nextcloud."""
    url = _full_url(relative_path)
    client = _get_client()
    resp = await client.request("DELETE", url, auth=_auth())
    if resp.status_code not in (200, 204, 404):
        raise HTTPException(status_code=502, detail=f"Nextcloud delete failed: {resp.status_code}")


async def rename_folder(old_relative_path: str, new_relative_path: str) -> None:
    """Rename/move a folder in Nextcloud using WebDAV MOVE."""
    url = _full_url(old_relative_path)
    destination = _full_url(new_relative_path)
    client = _get_client()
    resp = await client.request(
        "MOVE",
        url,
        auth=_auth(),
        headers={"Destination": destination, "Overwrite": "F"},
    )
    if resp.status_code not in (201, 204):
        raise HTTPException(status_code=502, detail=f"Nextcloud rename failed: {resp.status_code}")
