import json
import os
import time
from datetime import datetime, timedelta
from typing import Dict, Optional, Tuple, List

from modules.logger_tool import initialise_logger
from modules.database.services.neo4j_service import Neo4jService
from modules.database.init import init_user
from modules.database.tools.supabase_storage_tools import SupabaseStorageTools
from modules.database.schemas.nodes.schools.schools import SchoolNode
import modules.database.tools.neontology_tools as neon
from modules.database.tools.neontology_tools import create_or_merge_neontology_node
from modules.database.supabase.utils.client import SupabaseServiceRoleClient

logger = initialise_logger(__name__, os.getenv("LOG_LEVEL"), os.getenv("LOG_PATH"), 'default', True)

_CC_USERS_DB = "cc.users"
_CC_SCHOOLS_DB = "cc.institutes"
_DEFAULT_INSTITUTE_NAME = os.getenv("DEFAULT_INSTITUTE_NAME", "KevlarAI")
_DEFAULT_INSTITUTE_ID = os.getenv("DEFAULT_INSTITUTE_ID")


class ProvisioningService:
    """Coordinates provisioning of Neo4j resources for schools and users."""

    def __init__(self):
        self.neo4j_service = Neo4jService()
        self.supabase = SupabaseServiceRoleClient().supabase

    # ------------------------------------------------------------------
    # Naming helpers
    # ------------------------------------------------------------------
    @staticmethod
    def _sanitize_component(value: str) -> str:
        return "".join(ch for ch in value.lower() if ch.isalnum())

    def _build_user_db_name(self, role: str, user_id: str) -> str:
        return f"{_CC_USERS_DB}.{self._sanitize_component(role)}.{self._sanitize_component(user_id)}"

    def _build_school_db_name(self, institute_id: str) -> str:
        return f"{_CC_SCHOOLS_DB}.{self._sanitize_component(institute_id)}"

    # ------------------------------------------------------------------
    # Supabase helpers
    # ------------------------------------------------------------------
    def _get_profile(self, user_id: str) -> Dict:
        response = (
            self.supabase
            .table("profiles")
            .select("*")
            .eq("id", user_id)
            .single()
            .execute()
        )
        if not response.data:
            raise ValueError(f"Profile {user_id} not found")
        return response.data

    def _get_membership(self, profile_id: str) -> Optional[Dict]:
        response = (
            self.supabase
            .table("institute_memberships")
            .select("*")
            .eq("profile_id", profile_id)
            .limit(1)
            .execute()
        )
        data = response.data or []
        return data[0] if data else None

    def _get_institute(self, institute_id: str) -> Optional[Dict]:
        response = (
            self.supabase
            .table("institutes")
            .select("*")
            .eq("id", institute_id)
            .single()
            .execute()
        )
        return response.data if response and response.data else None

    def _get_institute_by_name(self, name: str) -> Optional[Dict]:
        response = (
            self.supabase
            .table("institutes")
            .select("*")
            .eq("name", name)
            .limit(1)
            .execute()
        )
        data = response.data or []
        return data[0] if data else None

    def _determine_membership_role(self, user_type: str) -> str:
        if "teacher" in user_type:
            return "teacher"
        if "student" in user_type:
            return "student"
        return "staff"

    def _ensure_membership(self, profile: Dict, user_type: str) -> Optional[Dict]:
        membership = self._get_membership(profile["id"])
        if membership:
            return membership

        institute_id = _DEFAULT_INSTITUTE_ID
        institute = None
        if institute_id:
            institute = self._get_institute(institute_id)
            if not institute:
                logger.warning(f"Default institute {_DEFAULT_INSTITUTE_ID} not found; attempting lookup by name")
                institute_id = None

        if not institute_id:
            institute = self._get_institute_by_name(_DEFAULT_INSTITUTE_NAME)
            if not institute:
                raise ValueError(f"Default institute '{_DEFAULT_INSTITUTE_NAME}' not found; cannot create membership")
            institute_id = institute["id"]

        role = self._determine_membership_role(user_type)

        try:
            response = (
                self.supabase
                .table("institute_memberships")
                .insert({
                    "profile_id": profile["id"],
                    "institute_id": institute_id,
                    "role": role
                })
                .execute()
            )
            data = response.data or []
            membership = data[0] if isinstance(data, list) and data else data
            email = profile.get("email") or profile.get("user_email") or profile.get("id")
            logger.info(f"Created institute membership for {email} -> {institute_id} as {role}")
            return membership
        except Exception as exc:
            logger.warning(f"Failed to create institute membership for {profile['id']}: {exc}")
            # Try to fetch again in case of race condition
            return self._get_membership(profile["id"])

    # ------------------------------------------------------------------
    # Provisioning actions
    # ------------------------------------------------------------------
    def ensure_school(self, institute_id: str) -> Dict[str, str]:
        """Ensure the Neo4j databases and root nodes exist for a school."""
        institute = self._get_institute(institute_id)
        if not institute:
            raise ValueError(f"Institute {institute_id} not found in Supabase")

        school_db = self._build_school_db_name(institute_id)
        curriculum_db = f"{school_db}.curriculum"

        # Ensure root namespaces exist
        self.neo4j_service.create_database(_CC_SCHOOLS_DB)
        self.neo4j_service.create_database(school_db)
        self.neo4j_service.create_database(curriculum_db)

        # Wait for databases to be fully available (Neo4j needs time to make new databases accessible)
        logger.info(f"Waiting for databases to be fully available...")
        time.sleep(2)  # Initial wait
        
        # Verify databases exist with retries
        max_retries = 5
        retry_delay = 1
        for attempt in range(max_retries):
            try:
                # Check if school_db exists
                check_result = self.neo4j_service.check_database_exists(school_db)
                if check_result.get("exists", False):
                    logger.info(f"Database {school_db} is available")
                    break
                else:
                    if attempt < max_retries - 1:
                        logger.info(f"Database {school_db} not yet available, retrying in {retry_delay}s... (attempt {attempt + 1}/{max_retries})")
                        time.sleep(retry_delay)
                    else:
                        logger.warning(f"Database {school_db} may not be fully available, proceeding anyway...")
            except Exception as e:
                logger.warning(f"Error checking database existence: {e}, proceeding anyway...")
                if attempt < max_retries - 1:
                    time.sleep(retry_delay)

        metadata = institute.get("metadata") or {}
        if isinstance(metadata, str):
            try:
                metadata = json.loads(metadata)
            except json.JSONDecodeError:
                metadata = {}

        school_type = metadata.get("school_type") or institute.get("school_type") or "demo"
        school_node = SchoolNode(
            uuid_string=self._sanitize_component(institute_id),
            node_storage_path=f"schools/{self._sanitize_component(institute_id)}/databases/{school_db}/{self._sanitize_component(institute_id)}",
            school_type=self._sanitize_component(school_type) or "demo",
            name=institute.get("name", "Unknown School"),
            website=institute.get("website", "https://example.com"),
        )

        # Retry node creation with exponential backoff
        max_node_retries = 3
        for attempt in range(max_node_retries):
            try:
                neon.init_neontology_connection()
                try:
                    create_or_merge_neontology_node(school_node, database=_CC_SCHOOLS_DB, operation='merge')
                    create_or_merge_neontology_node(school_node, database=school_db, operation='merge')
                    logger.info(f"Successfully created school nodes in databases")
                    break  # Success, exit retry loop
                finally:
                    neon.close_neontology_connection()
            except Exception as e:
                if "Database" in str(e) and "not found" in str(e):
                    if attempt < max_node_retries - 1:
                        wait_time = (attempt + 1) * 2  # Exponential backoff: 2s, 4s, 6s
                        logger.warning(f"Database not yet available, waiting {wait_time}s before retry (attempt {attempt + 1}/{max_node_retries}): {e}")
                        time.sleep(wait_time)
                    else:
                        logger.error(f"Failed to create school nodes after {max_node_retries} attempts: {e}")
                        raise
                else:
                    # Different error, don't retry
                    raise

        # Try to persist database references back to Supabase (best effort)
        updates = {
            "neo4j_private_db_name": school_db,
            "neo4j_private_sync_status": "ready",
            "neo4j_private_sync_at": datetime.utcnow().isoformat(),
            "neo4j_uuid_string": self._sanitize_component(institute_id),
        }
        try:
            (
                self.supabase
                .table("institutes")
                .update(updates)
                .eq("id", institute_id)
                .execute()
            )
        except Exception as exc:  # pragma: no cover - defensive logging only
            logger.warning(f"Failed to update institute {institute_id} with db info: {exc}")

        return {
            "db_name": school_db,
            "curriculum_db_name": curriculum_db,
            "school_node": school_node,
        }

    def ensure_user(self, user_id: str) -> Dict[str, Optional[str]]:
        """Provision Neo4j resources for a specific user profile."""
        profile = self._get_profile(user_id)
        user_type_raw = (profile.get("user_type") or "").lower()

        user_type_map = {
            "teacher": ("email_teacher", "teacher"),
            "email_teacher": ("email_teacher", "teacher"),
            "student": ("email_student", "student"),
            "email_student": ("email_student", "student"),
            "developer": ("developer", "developer"),
            "cc_developer": ("developer", "developer"),
            "admin": ("superadmin", "superadmin"),
            "super_admin": ("superadmin", "superadmin"),
            "superadmin": ("superadmin", "superadmin"),
            "platform_admin": ("superadmin", "superadmin"),
        }
        neo_user_type, worker_type = user_type_map.get(user_type_raw, (user_type_raw or "standard", user_type_raw or "standard"))

        user_db_name = profile.get("user_db_name")
        if not user_db_name:
            user_db_name = self._build_user_db_name(worker_type, user_id)

        full_name = profile.get("full_name") or profile.get("display_name") or profile.get("username") or "User"
        username = profile.get("username") or self._sanitize_component(profile.get("email", "user"))
        user_email = profile.get("email") or profile.get("user_email") or ""

        school_db_name = profile.get("school_db_name")
        school_node = None

        membership = None
        if worker_type in ("teacher", "student"):
            membership = self._ensure_membership(profile, user_type_raw)
            if not membership:
                raise ValueError("Unable to determine institute membership for school-based user")
        if membership:
            institute_id = membership.get("institute_id")
            if institute_id:
                ensure_school_result = self.ensure_school(institute_id)
                school_db_name = ensure_school_result["db_name"]
                school_meta = ensure_school_result.get("school_node")
                if isinstance(school_meta, SchoolNode):
                    school_node = school_meta
                else:
                    school_node = SchoolNode(
                        uuid_string=self._sanitize_component(institute_id),
                        node_storage_path="",
                        school_type=getattr(school_meta, "school_type", "demo") if school_meta else "demo",
                        name=(school_meta.get("name") if isinstance(school_meta, dict) else None) or "Unknown School",
                        website=(school_meta.get("website") if isinstance(school_meta, dict) else None) or "https://example.com",
                    )

        # Ensure base namespaces exist before creating user-specific db
        self.neo4j_service.create_database(_CC_USERS_DB)
        self.neo4j_service.create_database(user_db_name)

        calendar_start = datetime.utcnow().date()
        calendar_end = (datetime.utcnow() + timedelta(days=365)).date()

        # Initialize storage tools for user provisioning
        storage_tools = SupabaseStorageTools(user_db_name, init_run_type="user")

        init_user.create_user(
            user_id=user_id,
            user_type=neo_user_type,
            username=username,
            user_email=user_email,
            user_name=full_name,
            worker_name=full_name,
            worker_type=worker_type,
            worker_email=user_email,
            cc_users_db_name=_CC_USERS_DB,
            user_db_name=user_db_name,
            worker_db_name=school_db_name,
            calendar_start_date=calendar_start,
            calendar_end_date=calendar_end,
            school_node=school_node,
            storage_tools=storage_tools,
        )

        profile_updates = {
            "user_db_name": user_db_name,
            "school_db_name": school_db_name,
            "neo4j_sync_status": "ready",
            "neo4j_synced_at": datetime.utcnow().isoformat(),
        }
        try:
            (
                self.supabase
                .table("profiles")
                .update(profile_updates)
                .eq("id", user_id)
                .execute()
            )
        except Exception as exc:  # pragma: no cover - logging only
            logger.warning(f"Failed to update profile {user_id} with provisioning info: {exc}")

        return {
            "user_db_name": user_db_name,
            "worker_db_name": school_db_name,
            "worker_type": worker_type,
        }
