from __future__ import annotations

import json
import os
import threading
import time
from dataclasses import asdict, dataclass, field
from hashlib import sha256
from typing import Any, Dict, List, Optional

from oasyce.services.runtime_identity import normalize_runtime_actor


ASSET_STATE_OBSERVED = "observed"
ASSET_STATE_CANDIDATE = "candidate"
ASSET_STATE_DRAFT = "draft"
ASSET_STATE_LIVE = "live"
ASSET_STATE_DEPRECATED = "deprecated"
ASSET_STATE_ARCHIVED = "archived"

ASSET_STATES = {
    ASSET_STATE_OBSERVED,
    ASSET_STATE_CANDIDATE,
    ASSET_STATE_DRAFT,
    ASSET_STATE_LIVE,
    ASSET_STATE_DEPRECATED,
    ASSET_STATE_ARCHIVED,
}


def _now() -> int:
    return int(time.time())


def _local_ref(file_path: str) -> str:
    return sha256(os.path.abspath(file_path).encode("utf-8")).hexdigest()[:16]


def _valid_actor(actor: Any) -> Optional[Dict[str, Any]]:
    return normalize_runtime_actor(actor)


@dataclass
class AssetLifecycleEntry:
    state_key: str
    state: str
    created_at: int
    updated_at: int
    file_path: Optional[str] = None
    asset_id: Optional[str] = None
    owner: Optional[str] = None
    source: Optional[str] = None
    reason: Optional[str] = None
    suggested_name: Optional[str] = None
    tags: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)


class AssetLifecycleStore:
    """Minimal persistent asset lifecycle store.

    Keeps one local lifecycle record for discovered files and one durable
    live record for registered assets. This stays outside ledger/vault
    payloads so the state model can evolve without breaking asset schemas.
    """

    def __init__(self, data_dir: Optional[str] = None) -> None:
        if data_dir is None:
            data_dir = os.path.join(os.path.expanduser("~"), ".oasyce")
        self._data_dir = data_dir
        self._path = os.path.join(data_dir, "asset_lifecycle.json")
        self._lock = threading.Lock()
        self._entries: Dict[str, AssetLifecycleEntry] = {}
        self._load()

    def observe(
        self,
        *,
        file_path: str,
        source: str = "scanner",
        suggested_name: Optional[str] = None,
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        return self._upsert_local(
            file_path=file_path,
            state=ASSET_STATE_OBSERVED,
            source=source,
            suggested_name=suggested_name,
            tags=tags,
            metadata=metadata,
        )

    def candidate(
        self,
        *,
        file_path: str,
        source: str = "scanner",
        suggested_name: Optional[str] = None,
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        return self._upsert_local(
            file_path=file_path,
            state=ASSET_STATE_CANDIDATE,
            source=source,
            suggested_name=suggested_name,
            tags=tags,
            metadata=metadata,
        )

    def draft(
        self,
        *,
        file_path: str,
        reason: str,
        source: str = "inbox",
        suggested_name: Optional[str] = None,
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        return self._upsert_local(
            file_path=file_path,
            state=ASSET_STATE_DRAFT,
            source=source,
            reason=reason,
            suggested_name=suggested_name,
            tags=tags,
            metadata=metadata,
        )

    def live(
        self,
        *,
        asset_id: str,
        file_path: Optional[str] = None,
        owner: Optional[str] = None,
        source: str = "register",
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        if not asset_id:
            raise ValueError("asset_id is required for live state")
        return self._upsert(
            state_key=f"asset:{asset_id}",
            state=ASSET_STATE_LIVE,
            asset_id=asset_id,
            file_path=file_path,
            owner=owner,
            source=source,
            tags=tags,
            metadata=metadata,
        )

    def deprecated(
        self,
        *,
        asset_id: str,
        reason: str = "superseded",
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        return self._upsert(
            state_key=f"asset:{asset_id}",
            state=ASSET_STATE_DEPRECATED,
            asset_id=asset_id,
            reason=reason,
            metadata=metadata,
        )

    def archived(
        self,
        *,
        asset_id: Optional[str] = None,
        file_path: Optional[str] = None,
        reason: str = "archived",
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        state_key = f"asset:{asset_id}" if asset_id else f"local:{_local_ref(file_path or '')}"
        return self._upsert(
            state_key=state_key,
            state=ASSET_STATE_ARCHIVED,
            asset_id=asset_id,
            file_path=file_path,
            reason=reason,
            metadata=metadata,
        )

    def summary(self, recent_limit: int = 8) -> Dict[str, Any]:
        with self._lock:
            entries = list(self._entries.values())
        counts = {state: 0 for state in sorted(ASSET_STATES)}
        pricing_counts: Dict[str, int] = {}
        asset_class_counts: Dict[str, int] = {}
        policy_counts: Dict[str, int] = {}
        coordination_counts = {
            "principals": {},
            "devices": {},
            "agents": {},
        }
        for entry in entries:
            counts[entry.state] = counts.get(entry.state, 0) + 1
            price_model = str(
                entry.metadata.get("recommended_price_model")
                or entry.metadata.get("price_model")
                or ""
            ).strip()
            if price_model:
                pricing_counts[price_model] = pricing_counts.get(price_model, 0) + 1
            asset_class = str(entry.metadata.get("asset_class") or "").strip()
            if asset_class:
                asset_class_counts[asset_class] = asset_class_counts.get(asset_class, 0) + 1
            policy_reason = str(entry.metadata.get("portfolio_policy_reason") or "").strip()
            if policy_reason:
                policy_counts[policy_reason] = policy_counts.get(policy_reason, 0) + 1
            for actor_key in ("discovered_by", "recommended_by", "executed_by"):
                actor = _valid_actor(entry.metadata.get(actor_key))
                if not actor:
                    continue
                principal_id = str(((actor.get("principal") or {}).get("id")) or "").strip()
                device_id = actor.get("device_id") or ""
                agent_id = actor.get("agent_id") or ""
                if principal_id:
                    coordination_counts["principals"][principal_id] = (
                        coordination_counts["principals"].get(principal_id, 0) + 1
                    )
                if device_id:
                    coordination_counts["devices"][device_id] = (
                        coordination_counts["devices"].get(device_id, 0) + 1
                    )
                if agent_id:
                    coordination_counts["agents"][agent_id] = (
                        coordination_counts["agents"].get(agent_id, 0) + 1
                    )
        recent = sorted(entries, key=lambda item: item.updated_at, reverse=True)[:recent_limit]
        return {
            "counts": counts,
            "pricing_counts": pricing_counts,
            "asset_class_counts": asset_class_counts,
            "policy_counts": policy_counts,
            "coordination_counts": coordination_counts,
            "recent": [
                {
                    "state": entry.state,
                    "state_key": entry.state_key,
                    "asset_id": entry.asset_id,
                    "file_path": entry.file_path,
                    "owner": entry.owner,
                    "source": entry.source,
                    "reason": entry.reason,
                    "suggested_name": entry.suggested_name,
                    "tags": list(entry.tags),
                    "asset_class": entry.metadata.get("asset_class"),
                    "price_model": entry.metadata.get("recommended_price_model")
                    or entry.metadata.get("price_model"),
                    "portfolio_policy_reason": entry.metadata.get("portfolio_policy_reason"),
                    "discovered_by": _valid_actor(entry.metadata.get("discovered_by")),
                    "recommended_by": _valid_actor(entry.metadata.get("recommended_by")),
                    "executed_by": _valid_actor(entry.metadata.get("executed_by")),
                    "updated_at": entry.updated_at,
                }
                for entry in recent
            ],
        }

    def _upsert_local(
        self,
        *,
        file_path: str,
        state: str,
        source: str,
        reason: Optional[str] = None,
        suggested_name: Optional[str] = None,
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        return self._upsert(
            state_key=f"local:{_local_ref(file_path)}",
            state=state,
            file_path=file_path,
            source=source,
            reason=reason,
            suggested_name=suggested_name,
            tags=tags,
            metadata=metadata,
        )

    def _upsert(
        self,
        *,
        state_key: str,
        state: str,
        file_path: Optional[str] = None,
        asset_id: Optional[str] = None,
        owner: Optional[str] = None,
        source: Optional[str] = None,
        reason: Optional[str] = None,
        suggested_name: Optional[str] = None,
        tags: Optional[List[str]] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> AssetLifecycleEntry:
        if state not in ASSET_STATES:
            raise ValueError(f"invalid asset state: {state}")
        now = _now()
        with self._lock:
            entry = self._entries.get(state_key)
            if entry is None:
                entry = AssetLifecycleEntry(
                    state_key=state_key,
                    state=state,
                    created_at=now,
                    updated_at=now,
                )
                self._entries[state_key] = entry
            entry.state = state
            entry.updated_at = now
            if file_path is not None:
                entry.file_path = os.path.abspath(file_path)
            if asset_id is not None:
                entry.asset_id = asset_id
            if owner is not None:
                entry.owner = owner
            if source is not None:
                entry.source = source
            if reason is not None:
                entry.reason = reason
            if suggested_name is not None:
                entry.suggested_name = suggested_name
            if tags is not None:
                entry.tags = list(tags)
            if metadata:
                entry.metadata.update(dict(metadata))
            self._save_locked()
            return AssetLifecycleEntry(**asdict(entry))

    def _save_locked(self) -> None:
        os.makedirs(self._data_dir, exist_ok=True)
        payload = {key: asdict(value) for key, value in self._entries.items()}
        tmp = self._path + ".tmp"
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(payload, f, indent=2, ensure_ascii=False)
            f.flush()
            os.fsync(f.fileno())
        os.replace(tmp, self._path)

    def _load(self) -> None:
        if not os.path.exists(self._path):
            return
        try:
            with open(self._path, encoding="utf-8") as f:
                payload = json.load(f)
            if not isinstance(payload, dict):
                return
            for key, value in payload.items():
                if not isinstance(value, dict):
                    continue
                self._entries[key] = AssetLifecycleEntry(**value)
        except (OSError, json.JSONDecodeError, TypeError, ValueError):
            corrupt = self._path + ".corrupt"
            try:
                os.replace(self._path, corrupt)
            except OSError:
                pass


_STORE: Dict[str, AssetLifecycleStore] = {}
_STORE_LOCK = threading.Lock()


def get_asset_lifecycle_store(data_dir: Optional[str] = None) -> AssetLifecycleStore:
    key = os.path.abspath(data_dir or os.path.join(os.path.expanduser("~"), ".oasyce"))
    with _STORE_LOCK:
        if key not in _STORE:
            _STORE[key] = AssetLifecycleStore(data_dir=key)
        return _STORE[key]
