from __future__ import annotations

import itertools
from collections import defaultdict
from collections.abc import Iterator
from dataclasses import dataclass, field, asdict
from itertools import islice
from typing import Literal

import pandas as pd
from cognite.client.data_classes import Sequence, FileMetadata
from cognite.client.data_classes._base import CogniteResource
from deepdiff import DeepDiff  # type: ignore[import]

from cognite.powerops.resync.models.base.resource_type import Resource
from cognite.powerops.resync.models.base.cdf_resources import CDFResource


@dataclass
class Change:
    last: Resource
    new: Resource

    @property
    def changed_fields(self) -> str:
        last_dumped = (
            self.last.dump(camel_case=True) if isinstance(self.last, CogniteResource) else self.last.model_dump()
        )
        new_dumped = self.new.dump(camel_case=True) if isinstance(self.new, CogniteResource) else self.new.model_dump()
        return (
            DeepDiff(last_dumped, new_dumped, ignore_string_case=True)
            .pretty()
            .replace("added to dictionary.", f"will be added to {self.last.external_id}.")
            .replace("removed from dictionary.", f"will be removed from {self.last.external_id}.")
        )

    @property
    def is_changed_content(self) -> bool:
        if isinstance(self.last, (Sequence, FileMetadata)) and isinstance(self.new, (Sequence, FileMetadata)):
            last_hash = self.last.metadata.get(CDFResource.content_key_hash) if self.last.metadata else None
            new_hash = self.new.metadata.get(CDFResource.content_key_hash) if self.new.metadata else None
            return last_hash != new_hash or last_hash is None or new_hash is None
        return False


@dataclass
class FieldSummary:
    group: Literal["CDF", "Domain"]
    name: str
    added: int
    removed: int
    changed: int
    unchanged: int

    @property
    def total(self) -> int:
        return self.added + self.removed + self.changed + self.unchanged


@dataclass
class FieldIds:
    group: Literal["CDF", "Domain"]
    name: str
    added: list[str] = field(default_factory=list)
    removed: list[str] = field(default_factory=list)
    changed: list[str] = field(default_factory=list)
    unchanged: list[str] = field(default_factory=list)

    @property
    def total(self) -> int:
        return len(self.added) + len(self.removed) + len(self.changed) + len(self.unchanged)


@dataclass
class FieldDifference:
    group: Literal["CDF", "Domain"]
    name: str
    added: list[Resource] = field(default_factory=list)
    removed: list[Resource] = field(default_factory=list)
    changed: list[Change] = field(default_factory=list)
    unchanged: list[Resource] = field(default_factory=list)

    @property
    def total(self) -> int:
        return len(self.added) + len(self.removed) + len(self.changed) + len(self.unchanged)

    def as_summary(self) -> FieldSummary:
        return FieldSummary(
            group=self.group,
            name=self.name,
            added=len(self.added),
            removed=len(self.removed),
            changed=len(self.changed),
            unchanged=len(self.unchanged),
        )

    def as_ids(self, limit: int = -1) -> FieldIds:
        def get_identifier(item: Resource) -> str:
            if hasattr(item, "external_id"):
                return item.external_id or "missing external_id"
            elif hasattr(item, "name"):
                return item.name or "missing name"
            raise NotImplementedError(f"Could not find external_id or name in {item}")

        limit_ = limit if limit > 0 else None
        return FieldIds(
            group=self.group,
            name=self.name,
            added=[get_identifier(item) for item in islice(self.added, limit_)],
            removed=[get_identifier(item) for item in islice(self.removed, limit_)],
            changed=[get_identifier(item.last) for item in islice(self.changed, limit_)],
            unchanged=[get_identifier(item) for item in islice(self.unchanged, limit_)],
        )

    def set_set_dataset(self, dataset: int) -> None:
        for item in itertools.chain(self.added, self.removed, self.unchanged):
            if hasattr(item, "data_set_id"):
                item.data_set_id = dataset
        for item in self.changed:
            if hasattr(item.last, "data_set_id"):
                item.last.data_set_id = dataset
                item.new.data_set_id = dataset


@dataclass
class ModelDifference:
    name: str
    changes: dict[str, FieldDifference] = field(default_factory=dict)

    def __iter__(self) -> Iterator[FieldDifference]:
        return iter(self.changes.values())

    def __contains__(self, item):
        return item in self.changes

    def __getitem__(self, item):
        return self.changes[item]

    def __setitem__(self, key, value):
        self.changes[key] = value

    def has_changes(self, exclude: set | frozenset = frozenset({"timeseries"})) -> bool:
        return any(change.total != change.unchanged for change in self.changes.values() if change.name not in exclude)


@dataclass
class ModelDifferences:
    models: list[ModelDifference]

    def has_changes(self, exclude: set | frozenset = frozenset({"timeseries"})) -> bool:
        return any(m.has_changes(exclude) for m in self.models)

    def as_markdown(self) -> str:
        report = []
        for model in self.models:
            summaries = [change.as_summary() for change in model]
            table = pd.DataFrame(
                [
                    _exclude_keys(asdict(summary), keys=["group", "name"])
                    for summary in summaries
                    if summary.group == "CDF"
                ],
                index=[summary.name for summary in summaries if summary.group == "CDF"],
            )
            ids = [change.as_ids(limit=5) for change in model if change.group == "CDF"]
            added_ids = []
            for id_ in ids:
                if id_.added:
                    added_ids.append(f"#### Added {id_.name}\n{id_.added}")
            removed_ids = []
            for id_ in ids:
                if id_.removed:
                    removed_ids.append(f"#### Removed {id_.name}\n{id_.removed}")
            changed_samples = []
            for change, id_ in zip((c for c in model if c.group == "CDF"), ids):
                if change.changed:
                    new_line = "\n * "
                    sample_line = new_line.join(
                        f"{i}: {c.changed_fields}" for c, i in zip(change.changed[:3], id_.changed[:3])
                    )
                    changed_samples.append(f"#### Changed {change.name}\n{sample_line}")
            samples = []
            if added_ids:
                samples.append("\n".join(added_ids))
            if removed_ids:
                samples.append("\n".join(removed_ids))
            if changed_samples:
                samples.append("\n".join(changed_samples))
            if samples:
                samples.insert(0, "### Samples")
            samples = "\n".join(samples)

            report.append(
                f"""## {model.name}

{table.T.to_markdown()}

**NOTE** Timeseries are not updated by `resync`

{samples}
    """
            )
        return "\n".join(report)

    def as_markdown_summary(self, no_headers: bool = False, skip_domain: bool = True) -> str:
        by_name = defaultdict(list)
        for model in self.models:
            for change in model:
                if skip_domain and change.group == "Domain":
                    continue
                by_name[change.name].append(change)
        report = []
        for name, changes in by_name.items():
            added = sum(len(change.added) for change in changes)
            removed = sum(len(change.removed) for change in changes)
            change_count = sum(len(change.changed) for change in changes)
            report.append(f"* **{name}**: {added} added, {removed} removed, {change_count} changed")
        list_ = "\n".join(report)
        return f"""{'**' if no_headers else '##'} Summary {'**' if no_headers else ''}
{list_ if list_ else 'No changes'}
"""


def _exclude_keys(d: dict, keys: list[str]) -> dict:
    return {k: v for k, v in d.items() if k not in keys}
