# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.

import xml.etree.ElementTree as ET

import requests

from requests.auth import HTTPBasicAuth

from trytond.i18n import gettext
from trytond.model import ModelView, fields
from trytond.pool import Pool, PoolMeta

from .exceptions import DAVConnectionTestError


class User(metaclass=PoolMeta):
    __name__ = "res.user"
    webdav_url = fields.Char("WebDAV URL",
        help="The base URL to your WebDAV server\n"
            "e.g. http://<host>:<port>/remote.php/dav/")
    webdav_username = fields.Char("WebDAV Username",
        help="The name of the user used to authenticate with "
        "the WebDAV server.")
    webdav_password = fields.Char("WebDAV Password",
        help="The password of the authenticating user.\n"
        "Preferably use an app password created in the backend.")
    carddav_collections = fields.One2Many('webdav.carddav_collection',
        'user', 'CardDAV Collections (Addressbooks)')
    caldav_collections = fields.One2Many('webdav.caldav_collection',
        'user', 'CalDAV Collections (Calendars)')

    @classmethod
    def __setup__(cls):
        super().__setup__()
        cls._buttons.update({
            'test_webdav_connection': {},
            'update_dav_collections': {},
            })

    @property
    def base_url(self):
        return self.webdav_url.rstrip('/')

    def dav_request(
            self,
            url,
            method,
            headers=None,
            data=None,
            xml=None,
            expected=(200, 201, 204, 207),
            depth=None,
            timeout=10,
            retry_if_missing=False,
            mkcol_callback=None
            ):
        '''
        Generic DAV request handler supporting PROPFIND, MKCOL,
        PUT, DELETE, MOVE, COPY, REPORT etc.

        Args:
            url: Full WebDAV endpoint URL
            method: HTTP method (
                e.g. 'PROPFIND', 'MKCOL', 'PUT', 'DELETE', 'REPORT')
            headers: Dict of custom HTTP headers
            data: Raw request body (for PUT)
            xml: XML Element or string for XML-based methods (
                PROPFIND, MKCOL, REPORT)
            expected: Tuple of acceptable HTTP status codes
            depth: Optional DAV 'Depth' header value
            timeout: Request timeout in seconds
            retry_if_missing: If True, and MOVE/COPY fails with 409,
                will call mkcol_callback
            mkcol_callback: Callable(base_url) creates destination collection

        Returns:
            A dict with:
              - status: int (HTTP status code)
              - headers: dict
              - xml: parsed ElementTree (if XML)
              - text: raw response text
              - ok: boolean (status in expected)
        '''
        sess = requests.Session()
        sess.auth = (self.webdav_username, self.webdav_password)

        final_headers = {
            "User-Agent": "Tryton-DAVClient/1.0",
            "Accept": "*/*",
        }
        if depth is not None:
            final_headers["Depth"] = str(depth)
        if headers:
            final_headers.update(headers)

        # Encode XML if provided
        body = None
        if xml is not None:
            if isinstance(xml, ET.Element):
                body = ET.tostring(xml, encoding="utf-8", xml_declaration=True)
            elif isinstance(xml, str):
                body = xml.encode("utf-8")
            final_headers["Content-Type"] = "application/xml; charset=utf-8"
        elif data is not None:
            body = data

        response = sess.request(
            method=method.upper(),
            url=url,
            headers=final_headers,
            data=body,
            timeout=timeout,
            )

        # If MOVE/COPY failed due to missing collection and retry is enabled
        if (retry_if_missing
                and response.status_code == 409
                and method.upper() in ("MOVE", "COPY")
                and mkcol_callback):
            mkcol_callback
            response = sess.request(
                method=method.upper(),
                url=url,
                headers=final_headers,
                data=body,
                timeout=timeout,
                )

        result = {
            "status_code": response.status_code,
            "headers": dict(response.headers),
            "text": response.text or response.reason,
            "xml": None,
            "ok": response.status_code in expected,
            }

        # Try parsing XML if Content-Type matches
        if "xml" in response.headers.get("Content-Type", "").lower():
            try:
                result["xml"] = ET.fromstring(response.content)
            except ET.ParseError:
                pass

        return result

    def dav_move(
            self,
            old_url,
            new_url,
            service="vcard",
            overwrite=True,
            retry_if_missing=True,
            ):
        """
        Move a resource (e.g. vCard) from old_url to new_url.
        Automatically creates destination collection if missing.
        """
        pool = Pool()
        CardDAVCollection = pool.get('webdav.carddav_collection')
        CalDAVCollection = pool.get('webdav.caldav_collection')

        if service == 'vcard':
            model = CardDAVCollection
        else:
            model = CalDAVCollection

        search_url = new_url.split('/')[-2].replace('.vcf', '')
        collection, = model.search([
                ('url', '=', search_url),
                ('user', '=', self.id),
                ], limit=1)
        return self.dav_request(
            method="MOVE",
            url=old_url,
            headers={
                "Destination": new_url,
                "Overwrite": "T" if overwrite else "F",
            },
            expected=(201, 204),
            retry_if_missing=retry_if_missing,
            mkcol_callback=self.ensure_dav_collection(
                collection, service=service),
            )

    def dav_copy(
            self,
            old_url,
            new_url,
            service="vcard",
            overwrite=True,
            retry_if_missing=True,
            ):
        """
        Copy a resource (e.g. vCard) from old_url to new_url.
        """
        pool = Pool()
        CardDAVCollection = pool.get('webdav.carddav_collection')
        CalDAVCollection = pool.get('webdav.caldav_collection')

        if service == 'vcard':
            model = CardDAVCollection
        else:
            model = CalDAVCollection

        search_url = new_url.split('/')[-2].replace('.vcf', '')
        collection, = model.search([
                ('url', '=', search_url),
                ('user', '=', self.id),
                ], limit=1)
        return self.dav_request(
            method="COPY",
            url=old_url,
            headers={
                "Destination": new_url,
                "Overwrite": "T" if overwrite else "F",
            },
            expected=(201, 204),
            retry_if_missing=retry_if_missing,
            mkcol_callback=self.ensure_dav_collection(
                collection, service=service),
            )

    def list_dav_collections(self, url, service="carddav"):
        """
        List CardDAV/CalDAV collections

        For the sake of simplicity provide already here nextcloud compatibility
        """

        # XML-Namespace-Map
        ns = {
            "d": "DAV:",
            "card": "urn:ietf:params:xml:ns:carddav",
            "cal": "urn:ietf:params:xml:ns:caldav",
            "cs": "http://calendarserver.org/ns/",
            "nc": "http://nextcloud.org/ns/",
            }

        if service == "carddav":
            namespace = 'xmlns:card="urn:ietf:params:xml:ns:carddav"'
            description = '<card:addressbook-description/>'
        else:
            namespace = 'xmlns:cal="urn:ietf:params:xml:ns:caldav"'
            description = '<cal:calendar-description/>'

        body = f"""<?xml version="1.0"?>
        <d:propfind xmlns:d="DAV:"
                    {namespace}
                    xmlns:cs="http://calendarserver.org/ns/"
                    xmlns:nc="http://nextcloud.org/ns/">
          <d:prop>
            <d:displayname/>
            {description}
            <cs:getctag/>
            <nc:ctag/>
          </d:prop>
        </d:propfind>"""

        response = self.dav_request(url, 'PROPFIND', depth=1, xml=body)
        tree = response['xml']

        collections = []
        for resp in tree.findall("d:response", ns):
            href = resp.findtext("d:href", namespaces=ns)
            url = href.split('/')[-2]
            displayname = resp.findtext(
                "d:propstat/d:prop/d:displayname", namespaces=ns)
            desc = (
                resp.findtext(
                    "d:propstat/d:prop/card:addressbook-description",
                    namespaces=ns)
                or resp.findtext(
                    "d:propstat/d:prop/cal:calendar-description",
                    namespaces=ns)
            )

            # Suche nach ctag oder getctag (Nextcloud compat 23-30)
            ctag = (
                resp.findtext("d:propstat/d:prop/cs:getctag", namespaces=ns)
                or resp.findtext("d:propstat/d:prop/nc:ctag", namespaces=ns)
            )
            collection = {
                'url': url,
                'displayname': displayname,
                'description': desc,
                'ctag': ctag,
                }
            collections.append(collection)
        return collections

    def ensure_dav_collection(self, collection, service="carddav"):
        pool = Pool()
        CardDAVCollection = pool.get('webdav.carddav_collection')

        dav_url = (f'{self.base_url}/addressbooks/users/'
            f'{self.webdav_username}/{collection.url}/')
        propfind_body = """<?xml version="1.0"?>
        <d:propfind xmlns:d="DAV:">
            <d:prop><d:displayname/></d:prop>
        </d:propfind>"""
        request = self.dav_request(
            dav_url, 'PROPFIND', xml=propfind_body)

        if request['status_code'] == 207:
            #print(f"Addressbook '{collection.name}' already exists.")
            return dav_url
        elif request['status_code'] == 404:
            # Handle duplicate display names by generating a unique URL
            # and loop as long as a free candidate is found to avoid any
            # collision.  Store the URL of the last candidate as the
            # final URL of the collection.
            #print(f"Addressbook '{collection.name}' not found, creating...")
            suffix = 1
            while True:
                candidate_url = (
                    f"{dav_url}_{suffix}/" if suffix > 1 else f"{dav_url}")
                request = self.dav_request(candidate_url, 'PROPFIND', xml=propfind_body)
                if request['status_code'] == 404:
                    # Free URL found
                    break
                suffix += 1

            mkcol_body = f"""<?xml version="1.0"?>
            <d:mkcol xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav">
                <d:set>
                    <d:prop>
                      <d:resourcetype>
                        <d:collection/>
                        <card:addressbook/>
                      </d:resourcetype>
                      <d:displayname>{collection.name}</d:displayname>
                      <card:addressbook-description>{collection.description}</card:addressbook-description>
                    </d:prop>
                </d:set>
            </d:mkcol>"""
            mk = self.dav_request(candidate_url, "MKCOL", xml=mkcol_body)
            if mk['status_code'] in (201, 200):
                #print(f"Addressbook '{dav_url}' created successfully.")
                # Update the collection URL in a separate transaction via queue
                # if needed.
                final_url = candidate_url.split('/')[-2]
                if collection.url != final_url:
                    CardDAVCollection.__queue__.update_url(collection,
                        collection.url)
                return candidate_url
            status = gettext('dav_client.msg_dav_unexpected',
                    status_code=mk['status_code'],
                    reason=mk['text'])
            raise DAVConnectionTestError(gettext(
                    'dav_client.msg_dav_status',
                    status=status))

        else:
            status = gettext('dav_client.msg_dav_unexpected',
                    status_code=request['status_code'],
                    reason=request['text'])
            raise DAVConnectionTestError(gettext(
                    'dav_client.msg_dav_status',
                    status=status))

    @classmethod
    @ModelView.button
    def test_webdav_connection(cls, records):
        for record in records:
            try:
                response = requests.request(
                    "PROPFIND",
                    record.webdav_url,
                    auth=HTTPBasicAuth(
                        record.webdav_username, record.webdav_password),
                    headers={"Depth": "0"},
                    timeout=10,
                    )
                if response.status_code in (200, 207):
                    status = gettext('dav_client.msg_dav_success')
                elif response.status_code == 401:
                    status = gettext('dav_client.msg_dav_401')
                else:
                    status = gettext('dav_client.msg_dav_unexpected',
                        status_code=response.status_code,
                        reason=response.reason)
                raise DAVConnectionTestError(gettext(
                        'dav_client.msg_dav_status',
                        status=status))
            except requests.exceptions.RequestException as e:
                raise DAVConnectionTestError(gettext(
                        'dav_client.msg_dav_status',
                        status=e))

    @classmethod
    @ModelView.button
    def update_dav_collections(cls, records):
        cls.update_carddav_collection(records)
        cls.update_caldav_collection(records)

    @classmethod
    def update_carddav_collection(cls, records):
        pool = Pool()
        CardDAVCollection = pool.get('webdav.carddav_collection')

        for record in records:
            username = record.webdav_username
            carddav_url = (
                f"{record.base_url}/addressbooks/users/{username}/")
            collections = record.list_dav_collections(
                carddav_url, 'carddav')

            card_collections = []
            for collection in collections:
                displayname = collection['displayname']
                url = collection['url']
                if (displayname
                        and url
                        and not url.startswith('z-')  # system
                        and '_shared_by_' not in url):  # shared
                    stored_collections = CardDAVCollection.search([
                        ('user', '=', record.id),
                        ('url', '=', collection['url']),
                        ], limit=1)
                    if stored_collections:
                        stored_collection = stored_collections[0]
                        if stored_collection.ctag == collection['ctag']:
                            continue
                        card_collection = stored_collection
                    else:
                        card_collection = CardDAVCollection()
                        card_collection.user = record
                        card_collection.url = collection['url']
                    card_collection.name = collection['displayname']
                    card_collection.description = collection['description']
                    card_collection.ctag = collection['ctag']
                    card_collections.append(card_collection)
            CardDAVCollection.save(card_collections)

    @classmethod
    def update_caldav_collection(cls, records):
        pool = Pool()
        CalDAVCollection = pool.get('webdav.caldav_collection')

        for record in records:
            username = record.webdav_username
            caldav_url = (
                f"{record.base_url}/calendars/{username}/")
            collections = record.list_dav_collections(
                caldav_url, 'caldav')

            cal_collections = []
            for collection in collections:
                displayname = collection['displayname']
                url = collection['url']
                if displayname and url:
                    stored_collections = CalDAVCollection.search([
                        ('user', '=', record.id),
                        ('url', '=', collection['url']),
                        ], limit=1)
                    if stored_collections:
                        stored_collection = stored_collections[0]
                        if stored_collection.ctag == collection['ctag']:
                            continue
                        cal_collection = stored_collection
                    else:
                        cal_collection = CalDAVCollection()
                        cal_collection.user = record
                        cal_collection.url = collection['url']
                    cal_collection.name = collection['displayname']
                    cal_collection.description = collection['description']
                    cal_collection.ctag = collection['ctag']
                    cal_collections.append(cal_collection)
            CalDAVCollection.save(cal_collections)
