from __future__ import annotations

import hashlib
import json
import math
import re
import shutil
from dataclasses import asdict
from datetime import datetime, timezone
from pathlib import Path
from threading import RLock
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence

from ins_pricing.governance.audit import AuditLogger
from ins_pricing.governance.registry import ModelRegistry
from ins_pricing.governance.release import ReleaseManager

from model_manager_tool.auth import AuthenticatedUser, AuthorizationError, RBACStore
from model_manager_tool.manager_excel_mixin import EXCEL_EXTENSIONS, MODEL_EXTENSIONS, ExcelModelManagerMixin


def _utcnow_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def _safe_segment(raw: str) -> str:
    text = (raw or "").strip()
    text = re.sub(r"[^a-zA-Z0-9._-]+", "_", text)
    return text.strip("._-") or "unknown"


def _ensure_json_object(text: str, *, field_name: str) -> Dict[str, Any]:
    cleaned = (text or "").strip()
    if not cleaned:
        return {}
    try:
        payload = json.loads(cleaned)
    except json.JSONDecodeError as exc:
        raise ValueError(f"{field_name} must be valid JSON object: {exc}") from exc
    if not isinstance(payload, dict):
        raise ValueError(f"{field_name} must be a JSON object.")
    return payload


class ModelManagerService(ExcelModelManagerMixin):
    """Central service for model ingestion, versioning, deployment and RBAC."""

    def __init__(self, base_dir: str | Path) -> None:
        self.base_dir = Path(base_dir).resolve()
        self.base_dir.mkdir(parents=True, exist_ok=True)
        self.artifact_dir = self.base_dir / "artifacts"
        self.artifact_dir.mkdir(parents=True, exist_ok=True)

        self.registry = ModelRegistry(self.base_dir / "registry.json")
        self.audit = AuditLogger(audit_dir=self.base_dir / "audit")
        self.release = ReleaseManager(
            state_dir=self.base_dir / "release_state",
            registry=self.registry,
            audit_logger=self.audit,
        )
        self.auth = RBACStore(self.base_dir / "rbac.json")
        self._lock = RLock()

    @staticmethod
    def parse_json_object(raw: str, *, field_name: str) -> Dict[str, Any]:
        return _ensure_json_object(raw, field_name=field_name)

    @staticmethod
    def _version_key(version: Any) -> tuple[int, ...]:
        text = str(version or "")
        values: List[int] = []
        for token in text.replace("-", ".").split("."):
            if token.isdigit():
                values.append(int(token))
                continue
            digits = "".join(ch for ch in token if ch.isdigit())
            values.append(int(digits) if digits else 0)
        return tuple(values or [0])

    @staticmethod
    def _artifact_kind(path: Path) -> str:
        suffix = path.suffix.lower()
        if suffix in EXCEL_EXTENSIONS:
            return "factor_table_excel"
        if suffix in MODEL_EXTENSIONS:
            return "model_file"
        if suffix in {".csv", ".parquet", ".feather"}:
            return "feature_table"
        return "attachment"

    @staticmethod
    def _sha256(path: Path) -> str:
        digest = hashlib.sha256()
        with path.open("rb") as fh:
            while True:
                chunk = fh.read(1024 * 1024)
                if not chunk:
                    break
                digest.update(chunk)
        return digest.hexdigest()

    @staticmethod
    def _unique_filename(filename: str, used_names: set[str]) -> str:
        candidate = Path(filename).name or "artifact.bin"
        stem = Path(candidate).stem
        suffix = Path(candidate).suffix
        idx = 1
        while candidate.lower() in used_names:
            candidate = f"{stem}_{idx}{suffix}"
            idx += 1
        used_names.add(candidate.lower())
        return candidate

    def _load_registry_payload(self) -> Dict[str, List[dict]]:
        if not self.registry.registry_path.exists():
            return {}
        with self.registry.registry_path.open("r", encoding="utf-8") as fh:
            payload = json.load(fh)
        if not isinstance(payload, dict):
            return {}
        output: Dict[str, List[dict]] = {}
        for model_name, versions in payload.items():
            if isinstance(versions, list):
                output[str(model_name)] = [item for item in versions if isinstance(item, dict)]
        return output

    def list_models(self) -> List[Dict[str, Any]]:
        payload = self._load_registry_payload()
        out: List[Dict[str, Any]] = []
        for model_name in sorted(payload.keys()):
            versions = sorted(payload[model_name], key=lambda item: self._version_key(item.get("version")))
            if not versions:
                continue
            latest = versions[-1]
            out.append(
                {
                    "model_name": model_name,
                    "version_count": len(versions),
                    "latest_version": str(latest.get("version", "")),
                    "latest_status": str(latest.get("status", "candidate")),
                    "versions": [str(item.get("version", "")) for item in versions],
                }
            )
        return out

    def list_model_versions(self, model_name: str) -> List[Dict[str, Any]]:
        payload = self._load_registry_payload()
        rows = payload.get(model_name, [])
        versions = sorted(rows, key=lambda item: self._version_key(item.get("version")))
        environments = self.list_environments()
        active_map: Dict[str, tuple[str, str]] = {}
        for env in environments:
            active = self.release.get_active(env)
            if active is not None:
                active_map[env] = (active.name, active.version)

        out: List[Dict[str, Any]] = []
        for item in versions:
            version = str(item.get("version", ""))
            active_envs = [
                env
                for env, active_pair in active_map.items()
                if active_pair[0] == model_name and active_pair[1] == version
            ]
            row = dict(item)
            row["active_envs"] = active_envs
            out.append(row)
        return out

    def import_model(
        self,
        *,
        model_name: str,
        version: str,
        actor: str,
        artifact_paths: Sequence[Path],
        model_type: str = "generic",
        metrics: Optional[Dict[str, Any]] = None,
        tags: Optional[Dict[str, Any]] = None,
        notes: Optional[str] = None,
        progress_callback: Optional[Callable[[Dict[str, Any]], None]] = None,
    ) -> Dict[str, Any]:
        def emit_progress(
            *,
            value: float,
            stage: str,
            message: str,
            completed_artifacts: int = 0,
            total_artifacts: int = 0,
        ) -> None:
            if progress_callback is None:
                return
            safe_value = max(0.0, min(1.0, float(value)))
            payload = {
                "value": safe_value,
                "stage": str(stage),
                "message": str(message),
                "completed_artifacts": int(max(0, completed_artifacts)),
                "total_artifacts": int(max(0, total_artifacts)),
            }
            try:
                progress_callback(payload)
            except Exception:
                pass

        model_name = (model_name or "").strip()
        version = (version or "").strip()
        actor = (actor or "").strip()
        if not model_name:
            raise ValueError("model_name is required")
        if not version:
            raise ValueError("version is required")
        if not actor:
            raise ValueError("actor is required")
        if not artifact_paths:
            raise ValueError("At least one artifact file is required.")
        total_artifacts = len(artifact_paths)
        emit_progress(
            value=0.02,
            stage="started",
            message=f"Starting import: 0/{total_artifacts} artifacts",
            completed_artifacts=0,
            total_artifacts=total_artifacts,
        )

        normalized_metrics: Dict[str, Any] = {}
        normalized_tags: Dict[str, str] = {}
        for key, value in (metrics or {}).items():
            if value is None:
                continue
            normalized_metrics[str(key)] = value
        for key, value in (tags or {}).items():
            if value is None:
                continue
            normalized_tags[str(key)] = str(value)

        safe_name = _safe_segment(model_name)
        safe_version = _safe_segment(version)
        timestamp_segment = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
        target_root = self.artifact_dir / safe_name / safe_version / timestamp_segment
        target_root.mkdir(parents=True, exist_ok=True)

        used_names: set[str] = set()
        manifest_rows: List[Dict[str, Any]] = []
        registry_artifacts: List[Dict[str, str]] = []
        excel_auto_configs: List[Dict[str, Any]] = []

        with self._lock:
            if self.registry.get_version(model_name, version) is not None:
                raise ValueError(f"Model '{model_name}' version '{version}' already exists.")

            try:
                for index, source in enumerate(artifact_paths, start=1):
                    emit_progress(
                        value=0.05 + ((index - 1) / max(total_artifacts, 1)) * 0.8,
                        stage="artifact_processing",
                        message=f"Processing artifact {index}/{total_artifacts}: {Path(source).name}",
                        completed_artifacts=index - 1,
                        total_artifacts=total_artifacts,
                    )
                    src = Path(source).resolve()
                    if not src.exists() or not src.is_file():
                        raise FileNotFoundError(f"Artifact not found: {src}")
                    final_name = self._unique_filename(src.name, used_names)
                    dst = target_root / final_name
                    shutil.copy2(src, dst)

                    kind = self._artifact_kind(dst)
                    rel_path = dst.relative_to(self.base_dir).as_posix()
                    meta: Dict[str, Any] = {
                        "original_name": src.name,
                        "stored_name": final_name,
                        "relative_path": rel_path,
                        "size_bytes": int(dst.stat().st_size),
                        "sha256": self._sha256(dst),
                        "kind": kind,
                    }
                    if dst.suffix.lower() in EXCEL_EXTENSIONS:
                        emit_progress(
                            value=0.05 + ((index - 1) / max(total_artifacts, 1)) * 0.8 + 0.02,
                            stage="excel_parsing",
                            message=f"Parsing Excel artifact {index}/{total_artifacts}: {final_name}",
                            completed_artifacts=index - 1,
                            total_artifacts=total_artifacts,
                        )
                        excel_profile = self._inspect_excel(dst)
                        meta["excel_profile"] = excel_profile
                        config_auto_import = excel_profile.get("config_auto_import")
                        if isinstance(config_auto_import, dict):
                            if (
                                bool(config_auto_import.get("has_structured_config"))
                                or bool(config_auto_import.get("dropdown_validations"))
                                or bool(config_auto_import.get("formula_cells"))
                            ):
                                excel_auto_configs.append(
                                    {
                                        "artifact_name": final_name,
                                        "artifact_path": rel_path,
                                        "config": config_auto_import,
                                    }
                                )

                    manifest_rows.append(meta)
                    registry_artifacts.append({"path": rel_path, "description": kind})
                    emit_progress(
                        value=0.05 + (index / max(total_artifacts, 1)) * 0.8,
                        stage="artifact_complete",
                        message=f"Completed artifact {index}/{total_artifacts}: {final_name}",
                        completed_artifacts=index,
                        total_artifacts=total_artifacts,
                    )

                emit_progress(
                    value=0.92,
                    stage="registering",
                    message="Registering model metadata",
                    completed_artifacts=total_artifacts,
                    total_artifacts=total_artifacts,
                )
                registry_record = {
                    "model_name": model_name,
                    "version": version,
                    "created_at": _utcnow_iso(),
                    "model_type": str(model_type or "generic"),
                    "metrics": normalized_metrics,
                    "tags": normalized_tags,
                    "artifacts": registry_artifacts,
                    "artifact_manifest": manifest_rows,
                    "status": "candidate",
                    "notes": notes,
                    "imported_by": actor,
                }
                if excel_auto_configs:
                    registry_record["excel_auto_configs"] = excel_auto_configs
                self.registry.register(registry_record)
            except Exception:
                shutil.rmtree(target_root, ignore_errors=True)
                raise

            self.audit.log(
                "model_imported",
                actor,
                metadata={
                    "model_name": model_name,
                    "version": version,
                    "artifact_count": len(manifest_rows),
                    "model_type": model_type,
                },
                note=notes,
            )
            emit_progress(
                value=1.0,
                stage="completed",
                message="Import completed",
                completed_artifacts=total_artifacts,
                total_artifacts=total_artifacts,
            )
            return self.registry.get(model_name, version=version)

    def deploy_model(
        self,
        *,
        env: str,
        model_name: str,
        version: str,
        actor: str,
        note: str = "",
    ) -> Dict[str, Any]:
        env = (env or "").strip()
        if not env:
            raise ValueError("env is required")
        state = self.release.deploy(
            env=env,
            name=model_name,
            version=version,
            actor=actor,
            note=note or None,
            update_registry_status=True,
            registry_status="production" if env == "production" else "deployed",
        )
        return {
            "env": state.env,
            "active": asdict(state.active) if state.active else None,
            "history_count": len(state.history),
            "updated_at": state.updated_at,
        }

    def rollback_environment(
        self,
        *,
        env: str,
        actor: str,
        note: str = "",
    ) -> Dict[str, Any]:
        env = (env or "").strip()
        if not env:
            raise ValueError("env is required")
        state = self.release.rollback(
            env=env,
            actor=actor,
            note=note or None,
            update_registry_status=True,
            registry_status="production" if env == "production" else "deployed",
        )
        return {
            "env": state.env,
            "active": asdict(state.active) if state.active else None,
            "history_count": len(state.history),
            "updated_at": state.updated_at,
        }

    def purge_all_models(
        self,
        *,
        actor: str,
        clear_audit: bool = False,
    ) -> Dict[str, Any]:
        actor = (actor or "").strip()
        if not actor:
            raise ValueError("actor is required")

        def _dir_stats(path: Path) -> Dict[str, int]:
            file_count = 0
            total_bytes = 0
            if path.exists():
                for item in path.rglob("*"):
                    if not item.is_file():
                        continue
                    file_count += 1
                    try:
                        total_bytes += int(item.stat().st_size)
                    except OSError:
                        pass
            return {"file_count": file_count, "total_bytes": total_bytes}

        release_state_dir = self.base_dir / "release_state"
        audit_dir = self.base_dir / "audit"

        with self._lock:
            registry_payload = self._load_registry_payload()
            model_count = len(registry_payload)
            version_count = sum(len(rows) for rows in registry_payload.values())

            artifact_stats = _dir_stats(self.artifact_dir)
            release_state_stats = _dir_stats(release_state_dir)
            audit_stats = _dir_stats(audit_dir) if clear_audit else {"file_count": 0, "total_bytes": 0}

            shutil.rmtree(self.artifact_dir, ignore_errors=True)
            self.artifact_dir.mkdir(parents=True, exist_ok=True)

            with self.registry.registry_path.open("w", encoding="utf-8") as fh:
                json.dump({}, fh, indent=2, ensure_ascii=True)

            shutil.rmtree(release_state_dir, ignore_errors=True)
            release_state_dir.mkdir(parents=True, exist_ok=True)

            if clear_audit:
                shutil.rmtree(audit_dir, ignore_errors=True)
                audit_dir.mkdir(parents=True, exist_ok=True)

            summary = {
                "purged_by": actor,
                "purged_at": _utcnow_iso(),
                "model_count": model_count,
                "version_count": version_count,
                "artifact_file_count": artifact_stats["file_count"],
                "artifact_total_bytes": artifact_stats["total_bytes"],
                "release_state_file_count": release_state_stats["file_count"],
                "release_state_total_bytes": release_state_stats["total_bytes"],
                "clear_audit": bool(clear_audit),
                "audit_file_count": audit_stats["file_count"],
                "audit_total_bytes": audit_stats["total_bytes"],
            }
            self.audit.log(
                "model_store_purged",
                actor,
                metadata=summary,
            )
            return summary

    def list_environments(self) -> List[str]:
        envs = {"staging", "production"}
        for path in (self.base_dir / "release_state").glob("*.json"):
            envs.add(path.stem)
        return sorted(envs)

    def get_environment_state(self, env: str) -> Dict[str, Any]:
        active = self.release.get_active(env)
        history = self.release.list_history(env)
        return {
            "env": env,
            "active": asdict(active) if active else None,
            "history": [asdict(item) for item in history],
        }

    def get_audit_logs(self, *, limit: int = 200, model_name: Optional[str] = None) -> List[Dict[str, Any]]:
        rows = self.audit.get_logs(model_name=model_name)
        rows = sorted(rows, key=lambda item: str(item.get("timestamp", "")), reverse=True)
        return rows[:max(1, int(limit))]

    # RBAC wrappers
    def authenticate(self, username: str, password: str) -> AuthenticatedUser:
        return self.auth.authenticate(username, password)

    def get_user_profile(self, username: str) -> AuthenticatedUser:
        return self.auth.get_user(username)

    def require_permission(self, username: str, permission: str) -> None:
        self.auth.require_permission(username, permission)

    def list_users(self) -> List[Dict[str, Any]]:
        return self.auth.list_users()

    def list_roles(self) -> Dict[str, Dict[str, Any]]:
        return self.auth.list_roles()

    def create_user(self, username: str, password: str, roles: Iterable[str]) -> Dict[str, Any]:
        return self.auth.create_user(username, password, roles)

    def set_user_roles(self, username: str, roles: Iterable[str]) -> Dict[str, Any]:
        return self.auth.set_user_roles(username, roles)

    def create_role(
        self,
        role_name: str,
        permissions: Iterable[str],
        *,
        description: str = "",
    ) -> Dict[str, Any]:
        return self.auth.create_role(role_name, permissions, description=description)

    @property
    def available_permissions(self) -> List[str]:
        roles = self.list_roles()
        permissions: set[str] = set()
        for role_cfg in roles.values():
            for perm in role_cfg.get("permissions", []):
                if isinstance(perm, str):
                    permissions.add(perm)
        return sorted(permissions)
