"""Askar storage implementation of VC holder interface."""

import json

from dateutil.parser import parse as dateutil_parser
from dateutil.parser import ParserError
from typing import Mapping, Sequence

from ...askar.profile import AskarProfile

from ..askar import AskarStorage, AskarStorageSearch, AskarStorageSearchSession
from ..record import StorageRecord

from .base import VCHolder, VCRecordSearch
from .vc_record import VCRecord
from .xform import VC_CRED_RECORD_TYPE


class AskarVCHolder(VCHolder):
    """Askar VC record storage class."""

    def __init__(self, profile: AskarProfile):
        """Initialize the Indy-SDK VC holder instance."""
        self._profile = profile

    def build_type_or_schema_query(self, uri_list: Sequence[str]) -> dict:
        """Build and return indy-specific type_or_schema_query."""
        type_or_schema_query = {}
        for uri in uri_list:
            q = {"$or": [{"type": uri}, {"schema": uri}]}
            if type_or_schema_query:
                if "$and" not in type_or_schema_query:
                    type_or_schema_query = {"$and": [type_or_schema_query]}
                type_or_schema_query["$and"].append(q)
            else:
                type_or_schema_query = q
        return type_or_schema_query

    async def store_credential(self, cred: VCRecord):
        """Add a new VC record to the store.

        Args:
            cred: The VCRecord instance to store
        Raises:
            StorageDuplicateError: If the record_id is not unique

        """
        record = vc_to_storage_record(cred)
        async with self._profile.session() as session:
            await AskarStorage(session).add_record(record)

    async def retrieve_credential_by_id(self, record_id: str) -> VCRecord:
        """Fetch a VC record by its record ID.

        Raises:
            StorageNotFoundError: If the record is not found

        """
        async with self._profile.session() as session:
            record = await AskarStorage(session).get_record(
                VC_CRED_RECORD_TYPE, record_id
            )
        return storage_to_vc_record(record)

    async def retrieve_credential_by_given_id(self, given_id: str) -> VCRecord:
        """Fetch a VC record by its given ID ('id' property).

        Raises:
            StorageNotFoundError: If the record is not found

        """
        async with self._profile.session() as session:
            record = await AskarStorage(session).find_record(
                VC_CRED_RECORD_TYPE, {"given_id": given_id}
            )
        return storage_to_vc_record(record)

    async def delete_credential(self, cred: VCRecord):
        """Remove a previously-stored VC record.

        Raises:
            StorageNotFoundError: If the record is not found

        """
        async with self._profile.session() as session:
            await AskarStorage(session).delete_record(vc_to_storage_record(cred))

    def search_credentials(
        self,
        contexts: Sequence[str] = None,
        types: Sequence[str] = None,
        schema_ids: Sequence[str] = None,
        issuer_id: str = None,
        subject_ids: str = None,
        proof_types: Sequence[str] = None,
        given_id: str = None,
        tag_query: Mapping = None,
        pd_uri_list: Sequence[str] = None,
    ) -> "VCRecordSearch":
        """Start a new VC record search.

        Args:
            contexts: An inclusive list of JSON-LD contexts to match
            types: An inclusive list of JSON-LD types to match
            schema_ids: An inclusive list of credential schema identifiers
            issuer_id: The ID of the credential issuer
            subject_ids: The IDs of credential subjects all of which to match
            proof_types: The signature suite types used for the proof objects.
            given_id: The given id of the credential
            tag_query: A tag filter clause

        """

        def _match_any(query: list, k, vals):
            if vals is None:
                pass
            elif len(vals) > 1:
                query.append({"$or": [{k: v for v in vals}]})
            else:
                query.append({k: vals[0]})

        def _make_custom_query(query):
            result = {}
            for k, v in query.items():
                if isinstance(v, (list, set)) and k != "$exist":
                    result[k] = [_make_custom_query(cl) for cl in v]
                elif k.startswith("$"):
                    result[k] = v
                else:
                    result[f"cstm:{k}"] = v
            return result

        query = []
        _match_any(query, "context", contexts)
        _match_any(query, "type", types)
        _match_any(query, "schema", schema_ids)
        _match_any(query, "subject", subject_ids)
        _match_any(query, "proof_type", proof_types)
        if issuer_id:
            query.append({"issuer_id": issuer_id})
        if given_id:
            query.append({"given_id": given_id})
        if tag_query:
            query.append(_make_custom_query(tag_query))
        if pd_uri_list:
            query.append(self.build_type_or_schema_query(pd_uri_list))
        query = {"$and": query} if query else None
        search = AskarStorageSearch(self._profile).search_records(
            VC_CRED_RECORD_TYPE, query
        )
        return AskarVCRecordSearch(search)


class AskarVCRecordSearch(VCRecordSearch):
    """Askar storage search for VC records."""

    def __init__(self, search: AskarStorageSearchSession):
        """Initialize the Askar VC record search."""
        self._search = search

    async def close(self):
        """Dispose of the search query."""
        await self._search.close()

    async def fetch(self, max_count: int = None) -> Sequence[VCRecord]:
        """Fetch the next list of VC records from the store.

        Args:
            max_count: Max number of records to return. If not provided,
              defaults to the backend's preferred page size

        Returns:
            A list of `VCRecord` instances

        """
        rows = await self._search.fetch(max_count)
        records = [storage_to_vc_record(r) for r in rows]
        try:
            records.sort(
                key=lambda v: dateutil_parser(v.cred_value.get("issuanceDate")),
                reverse=True,
            )
            return records
        except ParserError:
            return records


def storage_to_vc_record(record: StorageRecord) -> VCRecord:
    """Convert an Askar stored record into a VC record."""

    def _make_set(val) -> set:
        if isinstance(val, str):
            return {val}
        else:
            return set(val)

    cred_tags = {}
    contexts = set()
    types = set()
    schema_ids = set()
    subject_ids = set()
    proof_types = set()
    issuer_id = None
    given_id = None
    for tagname, tagval in (record.tags or {}).items():
        if tagname == "context":
            contexts = _make_set(tagval)
        elif tagname == "type":
            types = _make_set(tagval)
        elif tagname == "schema":
            schema_ids = _make_set(tagval)
        elif tagname == "subject":
            subject_ids = _make_set(tagval)
        elif tagname == "proof_type":
            proof_types = _make_set(tagval)
        elif tagname == "issuer_id":
            issuer_id = tagval
        elif tagname == "given_id":
            given_id = tagval
        elif tagname.startswith("cstm:"):
            cred_tags[tagname[5:]] = tagval
    return VCRecord(
        contexts=contexts,
        expanded_types=types,
        schema_ids=schema_ids,
        issuer_id=issuer_id,
        subject_ids=subject_ids,
        proof_types=proof_types,
        cred_value=json.loads(record.value),
        given_id=given_id,
        cred_tags=cred_tags,
        record_id=record.id,
    )


def vc_to_storage_record(cred: VCRecord) -> StorageRecord:
    """Convert a VC record into an Askar stored record."""
    tags = {}
    tags["context"] = set(cred.contexts)
    tags["type"] = set(cred.expanded_types)
    tags["schema"] = set(cred.schema_ids)
    tags["subject"] = set(cred.subject_ids)
    tags["proof_type"] = set(cred.proof_types)
    if cred.issuer_id:
        tags["issuer_id"] = cred.issuer_id
    if cred.given_id:
        tags["given_id"] = cred.given_id
    for tagname, tagval in (cred.cred_tags or {}).items():
        tags[f"cstm:{tagname}"] = tagval

    return StorageRecord(
        VC_CRED_RECORD_TYPE,
        json.dumps(cred.cred_value),
        tags,
        cred.record_id,
    )
