# =================================================================
#
# Authors: Tom Kralidis <tomkralidis@gmail.com>
#
# Copyright (c) 2025 Tom Kralidis
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# =================================================================

import logging
from urllib.parse import urlencode

from owslib import fes
from owslib.csw import CatalogueServiceWeb
from owslib.ows import ExceptionReport

from pygeoapi.crs import crs_transform
from pygeoapi.provider.base import (BaseProvider, ProviderConnectionError,
                                    ProviderInvalidQueryError,
                                    ProviderItemNotFoundError,
                                    ProviderQueryError)
from pygeoapi.util import bbox2geojsongeometry, get_typed_value

LOGGER = logging.getLogger(__name__)


class CSWFacadeProvider(BaseProvider):
    """CSW Facade provider"""

    def __init__(self, provider_def):
        """
        Initialize object

        :param provider_def: provider definition

        :returns: pygeoapi.provider.csw_facade.CSWFacadeProvider
        """

        super().__init__(provider_def)

        self.record_mappings = {
            'type': ('dc:type', 'type'),
            'title': ('dc:title', 'title'),
            'description': ('dct:abstract', 'abstract'),
            'keywords': ('dc:subject', 'subjects'),
            'date': ('dc:date', 'date'),
            'created': ('dct:created', 'created'),
            'updated': ('dct:modified', 'modified'),
            'rights': ('dc:rights', 'rights'),
            'language': ('dc:language', 'language')
        }

        self._fields = {}
        self.get_fields()

    def get_fields(self):
        """
         Get provider field information (names, types)

        :returns: dict of fields
        """

        if not self._fields:
            date_fields = ['date', 'created', 'updated']

            for key in self.record_mappings.keys():
                LOGGER.debug(f'key: {key}')
                self._fields[key] = {'type': 'string'}

                if key in date_fields:
                    self._fields[key]['format'] = 'date-time'

        return self._fields

    def get_domains(self, properties=[], current=False) -> tuple:
        """
        Get domains from dataset

        :param properties: `list` of property names
        :param current: `bool` of whether to provide list of live
                        values (default `False`)

        :returns: `tuple` of domains and whether they are based on the
                  current/live dataset
        """

        LOGGER.debug(f'Querying CSW: {self.data}')
        records = self.query()
        domains = {}

        if properties:
            keys = properties
        else:
            keys = records['features'][0]['properties'].keys()

        csw = self._get_csw()

        for key in keys:
            csw.getdomain(key, dtype='property')
            domains[key] = csw.results['values']

        return domains, True

    @crs_transform
    def query(self, offset=0, limit=10, resulttype='results',
              bbox=[], datetime_=None, properties=[], sortby=[],
              select_properties=[], skip_geometry=False, q=None, **kwargs):
        """
        CSW GetRecords query

        :param offset: starting record to return (default 0)
        :param limit: number of records to return (default 10)
        :param resulttype: return results or hit limit (default results)
        :param bbox: bounding box [minx,miny,maxx,maxy]
        :param datetime_: temporal (datestamp or extent)
        :param properties: list of tuples (name, value)
        :param sortby: list of dicts (property, order)
        :param select_properties: list of property names
        :param skip_geometry: bool of whether to skip geometry (default False)
        :param q: full-text search term(s)

        :returns: `dict` of GeoJSON FeatureCollection
        """

        constraints = []

        response = {
            'type': 'FeatureCollection',
            'features': []
        }

        LOGGER.debug('Processing query parameters')

        if bbox:
            LOGGER.debug('Processing bbox parameter')
            LOGGER.debug('Swapping coordinate axis order from xy to yx')
            bbox2 = [bbox[1], bbox[0], bbox[3], bbox[2]]
            constraints.append(fes.BBox(bbox2))

        if datetime_:
            date_property = self.record_mappings[self.time_field][0]
            LOGGER.debug('Processing datetime parameter')
            if '/' in datetime_:
                begin, end = datetime_.split('/')
                LOGGER.debug('Processing time extent')
                constraints.append(fes.PropertyIsGreaterThan(date_property, begin))  # noqa
                constraints.append(fes.PropertyIsLessThan(date_property, end))
            else:
                LOGGER.debug('Processing time instant')
                constraints.append(fes.PropertyIsEqualTo(date_property,
                                                         datetime_))

        for p in properties:
            LOGGER.debug(f'Processing property {p} parameter')
            if p[0] not in list(self.record_mappings.keys()):
                msg = f'Invalid property: {p[0]}'
                LOGGER.error(msg)
                raise ProviderInvalidQueryError(user_msg=msg)

            prop = self.record_mappings[p[0]][0]
            constraints.append(fes.PropertyIsEqualTo(prop, p[1]))

        if q is not None:
            LOGGER.debug('Processing q parameter')
            anytext = fes.PropertyIsLike(propertyname='csw:AnyText', literal=q,
                                         escapeChar='\\', singleChar='?',
                                         wildCard='*')
            constraints.append(anytext)

        if sortby:
            LOGGER.debug('Processing sortby parameter')
            sorts = []
            sort_orders = {
                '+': 'ASC',
                '-': 'DESC'
            }
            for s in sortby:
                sorts.append(fes.SortProperty(
                    self.record_mappings[s['property']][0],
                    sort_orders[s['order']]))
            sortby2 = fes.SortBy(sorts)
        else:
            sortby2 = None

        if len(constraints) > 1:
            constraints = [fes.And(constraints)]

        LOGGER.debug(f'Querying CSW: {self.data}')
        csw = self._get_csw()
        try:
            csw.getrecords2(esn='full', maxrecords=limit, startposition=offset,
                            constraints=constraints, sortby=sortby2,
                            resulttype=resulttype)
        except ExceptionReport as err:
            msg = f'CSW error {err}'
            LOGGER.error(msg)
            raise ProviderQueryError(msg)

        response['numberMatched'] = csw.results['matches']
        response['numberReturned'] = csw.results['returned']
        LOGGER.debug(f"Found {response['numberMatched']} records")
        LOGGER.debug(f"Returned {response['numberReturned']} records")

        LOGGER.debug('Building result set')
        for record in csw.records.values():
            response['features'].append(self._owslibrecord2record(record))

        return response

    @crs_transform
    def get(self, identifier, **kwargs):
        """
        CSW GetRecordById query

        :param identifier: feature id

        :returns: dict of single GeoJSON feature
        """

        csw = self._get_csw()
        csw.getrecordbyid([identifier], esn='full')

        if not csw.records:
            err = f'item {identifier} not found'
            LOGGER.error(err)
            raise ProviderItemNotFoundError(err)

        record_key = list(csw.records.keys())[0]

        return self._owslibrecord2record(csw.records[record_key])

    def _get_csw(self) -> CatalogueServiceWeb:
        """
        Helper function to lazy load a CSW

        returns: `owslib.csw.CatalogueServiceWeb`
        """

        try:
            return CatalogueServiceWeb(self.data)
        except Exception as err:
            err = f'CSW connection error: {err}'
            LOGGER.error(err)
            raise ProviderConnectionError(err)

    def _gen_getrecordbyid_link(self, identifier: str,
                                csw_version: str = '2.0.2') -> dict:
        """
        Helper function to generate a CSW GetRecordById URL

        :param identifier: `str` of record identifier
        :param csw_version: `str` of CSW version (default is `2.0.2`)

        :returns: `dict` of link object of GetRecordById URL
        """

        params = {
            'service': 'CSW',
            'version': csw_version,
            'request': 'GetRecordById',
            'id': identifier
        }

        return {
            'rel': 'alternate',
            'type': 'application/xml',
            'title': 'This document as XML',
            'href': f'{self.data}?{urlencode(params)}',
        }

    def _owslibrecord2record(self, record):
        LOGGER.debug(f'Transforming {record.identifier}')
        feature = {
            'id': record.identifier,
            'type': 'Feature',
            'geometry': None,
            'time': record.date or None,
            'properties': {},
            'links': [
                self._gen_getrecordbyid_link(record.identifier)
            ]
        }

        LOGGER.debug('Processing record mappings to properties')
        for key, value in self.record_mappings.items():
            prop_value = getattr(record, value[1])
            if prop_value not in [None, [], '']:
                feature['properties'][key] = prop_value

        if record.bbox is not None:
            LOGGER.debug('Adding bbox')
            bbox = [
                get_typed_value(record.bbox.minx),
                get_typed_value(record.bbox.miny),
                get_typed_value(record.bbox.maxx),
                get_typed_value(record.bbox.maxy)
            ]
            feature['geometry'] = bbox2geojsongeometry(bbox)

        if record.references:
            LOGGER.debug('Adding references as links')
            for link in record.references:
                feature['links'].append({
                    'title': link['scheme'],
                    'href': link['url']
                })
        if record.uris:
            LOGGER.debug('Adding URIs as links')
            for link in record.uris:
                feature['links'].append({
                    'title': link['name'],
                    'href': link['url']
                })

        return feature

    def __repr__(self):
        return f'<CSWFacadeProvider> {self.data}'
