#
#
#

from datetime import datetime
from logging import getLogger
from uuid import uuid4

from .. import __version__
from ..record import Record
from .base import BaseProcessor

# TODO: remove once we require python >= 3.11
try:  # pragma: no cover
    from datetime import UTC
except ImportError:  # pragma: no cover
    from datetime import timedelta, timezone

    UTC = timezone(timedelta())


def _keys(values):
    return set(v.split('=', 1)[0] for v in values)


class MetaProcessor(BaseProcessor):
    '''
    Add a special metadata record with timestamps, UUIDs, versions, and/or
    provider name. Will only be updated when there are other changes being made.
    A useful tool to aid in debugging and monitoring of DNS infrastructure.

    Timestamps or UUIDs can be useful in checking whether changes are
    propagating, either from a provider's backend to their servers or via AXFRs.

    Provider can be utilized to determine which DNS system responded to a query
    when things are operating in dual authority or split horizon setups.

    Creates a TXT record with the name configured with values based on processor
    settings. Values are in the form `key=<value>`, e.g.
    `time=2023-09-10T05:49:04.246953`

    processors:
      meta:
        class: octodns.processor.meta.MetaProcessor
        # The name to use for the meta record.
        # (default: meta)
        record_name: meta
        # Include a timestamp with a UTC value indicating the timeframe when the
        # last change was made.
        # (default: true)
        include_time: true
        # Include a UUID that can be utilized to uniquely identify the run
        # pushing data
        # (default: false)
        include_uuid: false
        # Include the provider id for the target where data is being pushed
        # (default: false)
        include_provider: false
        # Include the octoDNS version being used
        # (default: false)
        include_version: false
        # Extra values to set on the records
        # (default: None)
        #include_extra:
        #  key: val
        #  foo: env/BAR
    '''

    @classmethod
    def get_time(cls):
        return datetime.now(UTC).isoformat()

    @classmethod
    def get_uuid(cls):
        return str(uuid4())

    def __init__(
        self,
        id,
        record_name='meta',
        include_time=True,
        include_uuid=False,
        include_version=False,
        include_provider=False,
        include_extra=None,
        ttl=60,
    ):
        self.log = getLogger(f'MetaSource[{id}]')
        super().__init__(id)
        self.log.info(
            '__init__: record_name=%s, include_time=%s, include_uuid=%s, include_version=%s, include_provider=%s, include_extra=%s, ttl=%d',
            record_name,
            include_time,
            include_uuid,
            include_version,
            include_provider,
            include_extra,
            ttl,
        )
        self.record_name = record_name
        self.time = self.get_time() if include_time else None
        self.uuid = self.get_uuid() if include_uuid else None
        self.include_version = include_version
        self.include_provider = include_provider
        self.include_extra = (
            [f'{key}={val}' for key, val in include_extra.items()]
            if include_extra is not None
            else []
        )
        self.ttl = ttl

    def values(self, target_id):
        ret = []
        if self.include_version:
            ret.append(f'octodns-version={__version__}')
        if self.include_provider:
            ret.append(f'provider={target_id}')
        if self.time:
            ret.append(f'time={self.time}')
        if self.uuid:
            ret.append(f'uuid={self.uuid}')
        # these were previously converted into key=value or will otherwise be []
        ret.extend(self.include_extra)
        return ret

    def process_source_and_target_zones(self, desired, existing, target):
        meta = Record.new(
            desired,
            self.record_name,
            {'ttl': self.ttl, 'type': 'TXT', 'values': self.values(target.id)},
            # we may be passing in empty values here to be filled out later in
            # process_source_and_target_zones
            lenient=True,
        )
        desired.add_record(meta)
        return desired, existing

    def _is_up_to_date_meta(self, change, target_id):
        # always something so we can see if its type and name
        record = change.record
        # existing state, if there is one
        existing = getattr(change, 'existing', None)
        return (
            record._type == 'TXT'
            and record.name == self.record_name
            and existing is not None
            # don't care about the values here, just the fields/keys
            and _keys(self.values(target_id)) == _keys(existing.values)
        )

    def process_plan(self, plan, sources, target):
        if (
            plan
            and len(plan.changes) == 1
            and self._is_up_to_date_meta(plan.changes[0], target.id)
        ):
            # the only change is the meta record, and it's not meaningfully
            # changing so we don't actually want to make the update, meta should
            # only be enough to cause a plan on its own if the fields changed
            return None

        # There's more than one thing changing so meta should update and/or meta
        # is meaningfully changing or being created...
        return plan
