"""Deployment orchestration for Tellr on Databricks Apps.

This module provides the main setup/update/delete functions for deploying
the Tellr AI slide generator to Databricks Apps from a notebook.
"""

import logging
import os
import shutil
import uuid
from contextlib import contextmanager
from importlib import resources
from pathlib import Path
from string import Template
from typing import Iterator, Optional

import psycopg2
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.apps import (
    App,
    AppDeployment,
    AppResource,
    AppResourceDatabase,
    AppResourceDatabaseDatabasePermission,
    ComputeSize,
)
from databricks.sdk.service.database import DatabaseInstance
from databricks.sdk.service.workspace import ImportFormat

logger = logging.getLogger(__name__)


class DeploymentError(Exception):
    """Raised when deployment fails."""

    pass


def setup(
    lakebase_name: str,
    schema_name: str,
    app_name: str,
    app_file_workspace_path: str,
    lakebase_compute: str = "CU_1",
    app_compute: str = "MEDIUM",
    app_version: Optional[str] = None,
    description: str = "Tellr AI Slide Generator",
) -> dict:
    """Deploy Tellr to Databricks Apps.

    This function creates all necessary infrastructure and deploys the app:
    1. Creates/gets Lakebase database instance
    2. Generates requirements.txt with pinned app version
    3. Generates app.yaml with environment variables
    4. Uploads files to workspace
    5. Creates Databricks App with database resource
    6. Sets up database schema, creates tables, and seeds default data

    Args:
        lakebase_name: Name for the Lakebase database instance
        schema_name: PostgreSQL schema name for app tables
        app_name: Name for the Databricks App
        app_file_workspace_path: Workspace path to upload app files
        lakebase_compute: Lakebase capacity (CU_1, CU_2, CU_4, CU_8)
        app_compute: App compute size (MEDIUM, LARGE, LIQUID)
        app_version: Specific databricks-tellr-app version (default: latest)
        description: App description

    Returns:
        Dictionary with deployment info:
        - url: App URL
        - app_name: Created app name
        - lakebase_name: Database instance name
        - schema_name: Schema name
        - status: "created"

    Raises:
        DeploymentError: If deployment fails
    """
    print(f"🚀 Deploying Tellr to Databricks Apps...")
    print(f"   App name: {app_name}")
    print(f"   Workspace path: {app_file_workspace_path}")
    print(f"   Lakebase: {lakebase_name} (capacity: {lakebase_compute})")
    print(f"   Schema: {schema_name}")
    print()

    # Get workspace client (uses notebook auth)
    ws = WorkspaceClient()

    try:
        # Step 1: Create/get Lakebase instance
        print("📊 Setting up Lakebase database...")
        lakebase_result = _get_or_create_lakebase(ws, lakebase_name, lakebase_compute)
        print(f"   ✅ Lakebase: {lakebase_result['name']} ({lakebase_result['status']})")
        print()

        # Step 2: Generate and upload files
        print("📁 Preparing deployment files...")
        with _staging_dir(app_file_workspace_path) as staging:
            # Generate requirements.txt
            _write_requirements(staging, app_version)
            print("   ✓ Generated requirements.txt")

            # Generate app.yaml
            _write_app_yaml(staging, lakebase_name, schema_name)
            print("   ✓ Generated app.yaml")

            # Upload to workspace
            print(f"☁️  Uploading to: {app_file_workspace_path}")
            _upload_files(ws, staging, app_file_workspace_path)
            print("   ✅ Files uploaded")
        print()

        # Step 3: Create app
        print(f"🔧 Creating Databricks App: {app_name}")
        app = _create_app(
            ws,
            app_name=app_name,
            description=description,
            workspace_path=app_file_workspace_path,
            compute_size=app_compute,
            lakebase_name=lakebase_name,
        )
        print(f"   ✅ App created")
        if app.url:
            print(f"   🌐 URL: {app.url}")
        print()

        # Step 4: Set up database schema and permissions
        print("📊 Setting up database schema...")
        _setup_database_schema(ws, app, lakebase_name, schema_name)
        print(f"   ✅ Schema '{schema_name}' configured")
        print()

        # Step 5: Create tables and seed default data
        print("📋 Creating database tables...")
        _setup_database_tables(ws, lakebase_name, schema_name)
        print(f"   ✅ Tables created and seeded")
        print()

        print("✅ Deployment complete!")
        return {
            "url": app.url,
            "app_name": app_name,
            "lakebase_name": lakebase_name,
            "schema_name": schema_name,
            "status": "created",
        }

    except Exception as e:
        raise DeploymentError(f"Deployment failed: {e}") from e


def update(
    app_name: str,
    app_file_workspace_path: str,
    lakebase_name: str,
    schema_name: str,
    app_version: Optional[str] = None,
) -> dict:
    """Deploy a new version of an existing Tellr app.

    Updates the app files and triggers a new deployment.

    Args:
        app_name: Name of the existing Databricks App
        app_file_workspace_path: Workspace path with app files
        lakebase_name: Lakebase instance name
        schema_name: Schema name
        app_version: Specific databricks-tellr-app version (default: latest)

    Returns:
        Dictionary with deployment info

    Raises:
        DeploymentError: If update fails
    """
    print(f"🔄 Updating Tellr app: {app_name}")

    ws = WorkspaceClient()

    try:
        # Generate and upload updated files
        with _staging_dir(app_file_workspace_path) as staging:
            _write_requirements(staging, app_version)
            _write_app_yaml(staging, lakebase_name, schema_name)
            _upload_files(ws, staging, app_file_workspace_path)
            print("   ✅ Files updated")

        # Trigger new deployment
        print("   ⏳ Deploying...")
        deployment = AppDeployment(source_code_path=app_file_workspace_path)
        result = ws.apps.deploy_and_wait(app_name=app_name, app_deployment=deployment)
        print(f"   ✅ Deployment completed: {result.deployment_id}")

        app = ws.apps.get(name=app_name)
        if app.url:
            print(f"   🌐 URL: {app.url}")

        return {
            "url": app.url,
            "app_name": app_name,
            "deployment_id": result.deployment_id,
            "status": "updated",
        }

    except Exception as e:
        raise DeploymentError(f"Update failed: {e}") from e


def delete(app_name: str) -> dict:
    """Delete a Tellr app.

    Note: This does not delete the Lakebase instance or data.

    Args:
        app_name: Name of the app to delete

    Returns:
        Dictionary with deletion status

    Raises:
        DeploymentError: If deletion fails
    """
    print(f"🗑️  Deleting app: {app_name}")

    ws = WorkspaceClient()

    try:
        ws.apps.delete(name=app_name)
        print("   ✅ App deleted")
        return {"app_name": app_name, "status": "deleted"}
    except Exception as e:
        raise DeploymentError(f"Deletion failed: {e}") from e


# -----------------------------------------------------------------------------
# Internal functions
# -----------------------------------------------------------------------------


def _get_or_create_lakebase(
    ws: WorkspaceClient, database_name: str, capacity: str
) -> dict:
    """Get or create a Lakebase database instance."""
    try:
        # Check if exists
        existing = ws.database.get_database_instance(name=database_name)
        return {
            "name": existing.name,
            "status": "exists",
            "state": existing.state.value if existing.state else "UNKNOWN",
        }
    except Exception as e:
        error_str = str(e).lower()
        if "not found" not in error_str and "does not exist" not in error_str:
            raise

    # Create new instance
    instance = ws.database.create_database_instance_and_wait(
        DatabaseInstance(name=database_name, capacity=capacity)
    )
    return {
        "name": instance.name,
        "status": "created",
        "state": instance.state.value if instance.state else "RUNNING",
    }


def _write_requirements(staging_dir: Path, app_version: Optional[str]) -> None:
    """Generate requirements.txt with app package."""
    if app_version and not _is_valid_version(app_version):
        raise DeploymentError(
            f"Invalid app version '{app_version}'. Expected a PEP 440 version."
        )

    if app_version:
        package_line = f"databricks-tellr-app=={app_version}"
    else:
        package_line = "databricks-tellr-app"

    content = "\n".join(
        [
            "# Generated by databricks-tellr setup",
            package_line,
        ]
    )
    (staging_dir / "requirements.txt").write_text(content)


def _write_app_yaml(staging_dir: Path, lakebase_name: str, schema_name: str) -> None:
    """Generate app.yaml with environment variables."""
    template_content = _load_template("app.yaml.template")
    content = Template(template_content).substitute(
        LAKEBASE_INSTANCE=lakebase_name,
        LAKEBASE_SCHEMA=schema_name,
    )
    (staging_dir / "app.yaml").write_text(content)


def _load_template(template_name: str) -> str:
    """Load a template file from package resources."""
    try:
        # Python 3.9+ style
        files = resources.files("databricks_tellr") / "_templates" / template_name
        return files.read_text()
    except (TypeError, AttributeError):
        # Fallback for older Python
        with resources.open_text(
            "databricks_tellr._templates", template_name
        ) as f:
            return f.read()


def _is_valid_version(version: str) -> bool:
    try:
        from packaging.version import Version
    except ImportError:
        return True
    try:
        Version(version)
        return True
    except Exception:
        return False


@contextmanager
def _staging_dir(app_file_workspace_path: str) -> Iterator[Path]:
    base_path = Path(app_file_workspace_path)
    staging_dir = base_path / f".tellr_staging_{uuid.uuid4().hex}"
    try:
        staging_dir.mkdir(parents=True, exist_ok=False)
    except Exception as exc:
        raise DeploymentError(
            f"Failed to create staging directory at {staging_dir}: {exc}"
        ) from exc

    try:
        yield staging_dir
    finally:
        shutil.rmtree(staging_dir, ignore_errors=True)


def _upload_files(
    ws: WorkspaceClient, staging_dir: Path, workspace_path: str
) -> None:
    """Upload files from staging directory to workspace."""
    # Ensure directory exists
    try:
        ws.workspace.mkdirs(workspace_path)
    except Exception:
        pass  # May already exist

    # Upload each file
    for file_path in staging_dir.iterdir():
        if file_path.is_file():
            workspace_file_path = f"{workspace_path}/{file_path.name}"
            with open(file_path, "rb") as f:
                ws.workspace.upload(
                    workspace_file_path,
                    f,
                    format=ImportFormat.AUTO,
                    overwrite=True,
                )


def _create_app(
    ws: WorkspaceClient,
    app_name: str,
    description: str,
    workspace_path: str,
    compute_size: str,
    lakebase_name: str,
) -> App:
    """Create Databricks App with database resource."""
    compute_size_enum = ComputeSize(compute_size)

    # Database resource
    resources = [
        AppResource(
            name="app_database",
            database=AppResourceDatabase(
                instance_name=lakebase_name,
                database_name="databricks_postgres",
                permission=AppResourceDatabaseDatabasePermission.CAN_CONNECT_AND_CREATE,
            ),
        )
    ]

    # Create app
    app = App(
        name=app_name,
        description=description,
        compute_size=compute_size_enum,
        default_source_code_path=workspace_path,
        resources=resources,
        user_api_scopes=[
            "sql",
            "dashboards.genie",
            "catalog.tables:read",
            "catalog.schemas:read",
            "catalog.catalogs:read",
            "serving.serving-endpoints",
        ],
    )

    result = ws.apps.create_and_wait(app)

    # Trigger initial deployment
    deployment = AppDeployment(source_code_path=workspace_path)
    ws.apps.deploy_and_wait(app_name=app_name, app_deployment=deployment)

    # Refresh to get URL
    return ws.apps.get(name=app_name)


def _setup_database_schema(
    ws: WorkspaceClient, app: App, lakebase_name: str, schema_name: str
) -> None:
    """Set up database schema and grant permissions to app."""
    # Get app's service principal client ID
    client_id = None
    if hasattr(app, "service_principal_client_id") and app.service_principal_client_id:
        client_id = app.service_principal_client_id
    elif hasattr(app, "service_principal_id") and app.service_principal_id:
        client_id = str(app.service_principal_id)

    if not client_id:
        print("   ⚠️  Could not get app client ID - schema setup skipped")
        return

    # Get connection info
    instance = ws.database.get_database_instance(name=lakebase_name)
    user = ws.current_user.me().user_name

    # Generate credential
    cred = ws.database.generate_database_credential(
        request_id=str(uuid.uuid4()),
        instance_names=[lakebase_name],
    )

    # Connect and create schema
    conn = psycopg2.connect(
        host=instance.read_write_dns,
        port=5432,
        user=user,
        password=cred.token,
        dbname="databricks_postgres",
        sslmode="require",
    )
    conn.autocommit = True

    with conn.cursor() as cur:
        cur.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"')
        cur.execute(f'GRANT USAGE ON SCHEMA "{schema_name}" TO "{client_id}"')
        cur.execute(f'GRANT CREATE ON SCHEMA "{schema_name}" TO "{client_id}"')
        cur.execute(
            f'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "{schema_name}" TO "{client_id}"'
        )
        cur.execute(
            f'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "{schema_name}" TO "{client_id}"'
        )
        cur.execute(
            f'ALTER DEFAULT PRIVILEGES IN SCHEMA "{schema_name}" '
            f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "{client_id}"'
        )
        cur.execute(
            f'ALTER DEFAULT PRIVILEGES IN SCHEMA "{schema_name}" '
            f'GRANT USAGE, SELECT ON SEQUENCES TO "{client_id}"'
        )

    conn.close()


def _setup_database_tables(
    ws: WorkspaceClient, lakebase_name: str, schema_name: str
) -> None:
    """Create database tables and seed default data.

    This function:
    1. Creates all SQLAlchemy tables defined in the app models
    2. Seeds the slide_deck_prompt_library with generic templates
    3. Seeds the slide_style_library with the system default style

    Args:
        ws: WorkspaceClient for database access
        lakebase_name: Lakebase instance name
        schema_name: Schema name where tables will be created
    """
    # Get connection info
    instance = ws.database.get_database_instance(name=lakebase_name)
    user = ws.current_user.me().user_name

    # Generate credential
    cred = ws.database.generate_database_credential(
        request_id=str(uuid.uuid4()),
        instance_names=[lakebase_name],
    )

    # Build connection string for SQLAlchemy
    from urllib.parse import quote_plus

    encoded_password = quote_plus(cred.token)
    database_url = (
        f"postgresql://{user}:{encoded_password}@{instance.read_write_dns}:5432"
        f"/databricks_postgres?sslmode=require&options=-csearch_path%3D{schema_name}"
    )

    # Import SQLAlchemy components from the app package
    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker

    # Import models - these define the table schemas
    from src.core.database import Base
    from src.database.models import SlideDeckPromptLibrary, SlideStyleLibrary

    # Create engine and tables
    engine = create_engine(database_url, pool_pre_ping=True)
    Base.metadata.create_all(bind=engine)
    print("   ✓ Created database tables")

    # Create session for seeding data
    Session = sessionmaker(bind=engine)
    session = Session()

    try:
        # Seed deck prompts if empty
        existing_prompts = session.query(SlideDeckPromptLibrary).count()
        if existing_prompts == 0:
            prompts = _get_default_deck_prompts()
            for prompt_data in prompts:
                prompt = SlideDeckPromptLibrary(
                    name=prompt_data["name"],
                    description=prompt_data["description"],
                    category=prompt_data["category"],
                    prompt_content=prompt_data["prompt_content"],
                    is_active=True,
                    created_by="system",
                    updated_by="system",
                )
                session.add(prompt)
            session.commit()
            print(f"   ✓ Seeded {len(prompts)} deck prompts")
        else:
            print(f"   ✓ Deck prompts already exist ({existing_prompts})")

        # Seed slide styles if empty
        existing_styles = session.query(SlideStyleLibrary).count()
        if existing_styles == 0:
            styles = _get_default_slide_styles()
            for style_data in styles:
                style = SlideStyleLibrary(
                    name=style_data["name"],
                    description=style_data["description"],
                    category=style_data["category"],
                    style_content=style_data["style_content"],
                    is_active=True,
                    is_system=style_data.get("is_system", False),
                    created_by="system",
                    updated_by="system",
                )
                session.add(style)
            session.commit()
            print(f"   ✓ Seeded {len(styles)} slide styles")
        else:
            print(f"   ✓ Slide styles already exist ({existing_styles})")

    finally:
        session.close()
        engine.dispose()


def _get_default_deck_prompts() -> list[dict]:
    """Return generic deck prompt templates for seeding.

    These are business-agnostic templates suitable for any organization.
    """
    return [
        {
            "name": "Quarterly Business Review",
            "description": "Template for QBR presentations. Covers performance metrics, achievements, challenges, and strategic recommendations.",
            "category": "Report",
            "prompt_content": """PRESENTATION TYPE: Quarterly Business Review (QBR)

When creating a QBR presentation, structure it as follows:

1. QUARTER OVERVIEW
   - Executive summary of the quarter
   - Key performance indicators vs. targets
   - Major achievements and milestones

2. METRICS DEEP DIVE
   - Query for all relevant metrics for the quarter
   - Compare to previous quarter and same quarter last year
   - Use charts to visualize performance trends

3. SUCCESS STORIES
   - Highlight specific wins with data
   - Quantify impact where possible
   - Include growth or improvement percentages

4. CHALLENGES & LEARNINGS
   - Acknowledge areas that didn't meet expectations
   - Provide context with supporting data
   - Share lessons learned

5. NEXT QUARTER OUTLOOK
   - Goals and targets for upcoming quarter
   - Strategic initiatives planned
   - Resource requirements or asks

Use a professional, data-driven approach with clear visualizations for each section.""",
        },
        {
            "name": "Executive Summary",
            "description": "High-level overview format for executive audiences. Focuses on key metrics and strategic insights.",
            "category": "Summary",
            "prompt_content": """PRESENTATION TYPE: Executive Summary

When creating an executive summary presentation:

DESIGN PRINCIPLES:
- Keep it concise - executives have limited time
- Lead with insights, not data
- Use clear, impactful titles that state the takeaway
- Maximum 5-7 slides total

STRUCTURE:
1. HEADLINE SLIDE
   - Single most important insight or conclusion
   - One key metric that supports it

2. SITUATION OVERVIEW
   - Brief context (2-3 bullets max)
   - Current state summary

3. KEY FINDINGS (2-3 slides max)
   - One major insight per slide
   - Support each with 1-2 data points
   - Use simple charts (bar or line)

4. RECOMMENDATIONS
   - Clear, actionable next steps
   - Prioritized list (max 3 items)

5. ASK (if applicable)
   - What decision or action is needed
   - Required resources or support

Keep language simple and avoid jargon. Every data point should support a decision.""",
        },
    ]


def _get_default_slide_styles() -> list[dict]:
    """Return default slide styles for seeding.

    Returns the System Default style which cannot be edited or deleted.
    """
    # Import the default style content from the app
    try:
        from src.core.defaults import DEFAULT_SLIDE_STYLE

        style_content = DEFAULT_SLIDE_STYLE
    except ImportError:
        # Fallback minimal style if import fails
        style_content = """/* System Default Slide Style */
.slide {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    padding: 40px;
    background: #ffffff;
}
.slide h1 { font-size: 2.5em; color: #1a1a1a; margin-bottom: 0.5em; }
.slide h2 { font-size: 1.8em; color: #333333; margin-bottom: 0.5em; }
.slide p { font-size: 1.1em; line-height: 1.6; color: #444444; }
.slide ul, .slide ol { margin-left: 1.5em; }
.slide li { margin-bottom: 0.5em; }
"""

    return [
        {
            "name": "System Default",
            "description": "Protected system style. Use this as a template when creating your own custom styles.",
            "category": "System",
            "style_content": style_content,
            "is_system": True,
        },
    ]
