"""The central registry for multi-tenant SaaS applications - managing users, tenants, and access control."""

# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/00_db_host.ipynb.

# %% auto 0
__all__ = ['timestamp', 'gen_id', 'get_db_uri', 'GlobalUser', 'TenantCatalog', 'Membership', 'Subscription', 'HostAuditLog',
           'SystemJob', 'HostDatabase']

# %% ../nbs/00_db_host.ipynb 2
from fastsql import *
from fastcore.utils import *
import uuid
import os
import urllib.parse
from datetime import datetime
from nbdev.showdoc import show_doc

# %% ../nbs/00_db_host.ipynb 5
def timestamp(): return datetime.utcnow().isoformat()
def gen_id(): return uuid.uuid4().hex

# %% ../nbs/00_db_host.ipynb 6
def get_db_uri(db) -> str:
    """
    Extract SQLAlchemy connection URI from a Database object.
    
    Safely renders the URL with the actual password (required for
    utilities like `map_and_upsert` that create new connections).
    
    **Why this is needed:**
    - `str(db.conn.engine.url)` masks the password as `***`
    - This causes "password authentication failed" errors
    - `render_as_string(hide_password=False)` reveals the actual password
    
    | Parameter | Description |
    |-----------|-------------|
    | `db` | fastsql/minidataapi Database object or HostDatabase instance |
    
    **Returns:** Full connection URI string with password
    
    **Example:**
    ```python
    from fh_saas.db_host import get_db_uri, HostDatabase
    from fh_saas.utils_polars_mapper import map_and_upsert
    
    host_db = HostDatabase.from_env()
    db_uri = get_db_uri(host_db.db)
    
    # Now safe to use with map_and_upsert
    map_and_upsert(df, 'my_table', 'id', db_uri)
    ```
    """
    import logging
    logger = logging.getLogger(__name__)
    
    # Handle both Database objects and HostDatabase wrapper
    if hasattr(db, 'db'):
        # It's a HostDatabase wrapper, get the inner Database
        engine = db.db.conn.engine
    elif hasattr(db, 'conn'):
        # It's a raw Database object
        engine = db.conn.engine
    elif hasattr(db, 'engine'):
        # It's something with an engine directly
        engine = db.engine
    else:
        raise TypeError(f"Cannot extract URI from {type(db)}. Expected Database or HostDatabase.")
    
    db_uri = engine.url.render_as_string(hide_password=False)
    logger.info(f"Extracted database URI for {engine.url.database}")
    
    return db_uri

# %% ../nbs/00_db_host.ipynb 9
class GlobalUser:
    """Identity: Who is this person?"""
    id: str; email: str; oauth_id: str
    password_hash: str = None; stripe_cust_id: str = None
    is_sys_admin: bool = False; created_at: str; last_login: str = None

class TenantCatalog:
    """Registry: Where is the database?"""
    id: str; name: str; db_url: str
    is_active: bool = True; plan_tier: str = "free"; created_at: str

class Membership:
    """Router: Which tenants can they access?"""
    id: str; user_id: str; tenant_id: str; profile_id: str
    meta_attributes: str = None; role: str = "member"
    is_active: bool = True; created_at: str

class Subscription:
    """Billing: Are they allowed to use the app?"""
    id: str; tenant_id: str
    stripe_sub_id: str; stripe_cust_id: str
    plan_tier: str; status: str
    current_period_end: str; cancel_at_period_end: bool = False

class HostAuditLog:
    """Security: Who changed the system?"""
    id: str; actor_user_id: str; event_type: str
    target_id: str = None; details: str = None; ip_address: str = None
    created_at: str

class SystemJob:
    """Maintenance: Provisioning & Cleanups"""
    id: str; job_type: str; status: str
    payload: str = None; error_log: str = None
    created_at: str; completed_at: str = None



# %% ../nbs/00_db_host.ipynb 12
class HostDatabase:
    """Singleton connection manager for the host database."""
    _instance = None
    
    def __new__(cls, db_url: str = None):
        """Singleton pattern - only one instance per application."""
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance._initialized = False
        return cls._instance
    
    def __init__(self, db_url: str = None):
        """Initialize database connection and create table objects."""
        if self._initialized:
            return
        
        if db_url is None:
            raise ValueError("db_url required for first initialization")
        
        # Create database connection
        self.db = Database(db_url)
        
        # Create table objects for host schema
        self.global_users = self.db.create(GlobalUser, name="core_users", pk='id')
        self.tenant_catalogs = self.db.create(TenantCatalog, name="core_tenants", pk='id')
        self.memberships = self.db.create(Membership, name="core_memberships", pk='id')
        self.subscriptions = self.db.create(Subscription, name="core_subscriptions", pk='id')
        self.audit_logs = self.db.create(HostAuditLog, name="sys_audit_logs", pk='id')
        self.system_jobs = self.db.create(SystemJob, name="sys_jobs", pk='id')
        
        self._initialized = True
    
    @property
    def engine(self):
        """Get the underlying SQLAlchemy engine."""
        return self.db.engine
    
    @classmethod
    def from_env(cls):
        """Create HostDatabase from DB_* environment variables."""
        DB_TYPE = os.getenv("DB_TYPE", "POSTGRESQL")
        DB_USER = os.getenv("DB_USER", "postgres")
        DB_PASS = os.getenv("DB_PASS", "")
        DB_HOST = os.getenv("DB_HOST", "localhost")
        DB_PORT = os.getenv("DB_PORT", "5432")
        DB_NAME = os.getenv("DB_NAME", "app_host")
        
        if DB_TYPE == "POSTGRESQL":
            if not DB_PASS:
                raise ValueError("DB_PASS is required for PostgreSQL")
            encoded_pass = urllib.parse.quote_plus(DB_PASS)
            db_url = f"postgresql://{DB_USER}:{encoded_pass}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
        else:
            db_url = f"sqlite:///{DB_NAME}.db"
        
        return cls(db_url)
    
    def commit(self):
        """Commit current transaction."""
        self.db.conn.commit()
    
    def rollback(self):
        """Rollback current transaction."""
        self.db.conn.rollback()
    
    def close(self):
        """Close database connection and dispose engine.
        
        Call this when shutting down or before reset_instance() in tests.
        """
        try:
            self.db.conn.close()
        except Exception:
            pass
        try:
            self.db.engine.dispose()
        except Exception:
            pass
        self._initialized = False
    
    @classmethod
    def reset_instance(cls):
        """Reset singleton instance (testing only).
        
        ⚠️ Call close() first to release database connections!
        """
        if cls._instance is not None:
            try:
                cls._instance.close()
            except Exception:
                pass
        cls._instance = None
