"""First-run onboarding for GreyMatter Solo.

Orchestrates the guided setup that runs when Solo is installed:
1. Generate master encryption key (or validate existing)
2. Create the encrypted "personal" channel
3. Set default classification to Personal
4. Optionally register with a Team Hub

State machine:
    not_initialized → keys_generated → channel_created → defaults_set → hub_registered → complete

Each step is idempotent — re-running skips already-completed steps.
"""
from __future__ import annotations

import logging
import os
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

import httpx

from greymatter_core.db import SQLiteDatabase

logger = logging.getLogger(__name__)

# Classification levels — ordered from most private to most shareable.
CLASSIFICATION_LEVELS = ("personal", "team", "public")

DEFAULT_DATA_DIR = Path.home() / ".greymatter"
MASTER_KEY_PATH = DEFAULT_DATA_DIR / ".keyfile"
ONBOARDING_STATE_KEY = "onboarding_completed_at"
DEFAULT_CLASSIFICATION_KEY = "default_classification"
PERSONAL_CHANNEL_ID = "ch-personal"


@dataclass
class OnboardingResult:
    """Result of a first-run onboarding attempt."""

    master_key_generated: bool = False
    master_key_existed: bool = False
    personal_channel_created: bool = False
    personal_channel_existed: bool = False
    classification_set: bool = False
    hub_registered: bool = False
    hub_registration_error: str = ""
    completed: bool = False
    steps_completed: list[str] = field(default_factory=list)


def is_first_run(data_dir: Path | None = None) -> bool:
    """Check whether Solo has completed first-run onboarding.

    Returns True if onboarding has never completed (i.e., this IS a first run).
    """
    data_dir = data_dir or DEFAULT_DATA_DIR
    keyfile = data_dir / ".keyfile"
    db_path = data_dir / "greymatter.db"
    return not keyfile.exists() or not db_path.exists()


def generate_master_key(data_dir: Path | None = None) -> tuple[str, bool]:
    """Generate or load the master encryption key.

    The master key is a 256-bit random key stored in the keyfile with
    owner-only permissions (0o600). All other keys (SQLCipher DB key,
    sync payload key, channel keys) are derived from this master key
    using PBKDF2-HMAC-SHA256 with unique salts.

    Args:
        data_dir: GreyMatter data directory. Defaults to ~/.greymatter.

    Returns:
        Tuple of (hex_key, was_generated). was_generated is True if a new
        key was created, False if an existing key was loaded.
    """
    data_dir = data_dir or DEFAULT_DATA_DIR
    keyfile = data_dir / ".keyfile"

    if keyfile.exists():
        key = keyfile.read_text().strip()
        if key:
            logger.info("Master key loaded from %s", keyfile)
            return key, False

    # Generate new 256-bit key
    data_dir.mkdir(parents=True, exist_ok=True)
    key = os.urandom(32).hex()
    keyfile.write_text(key)
    keyfile.chmod(0o600)
    logger.info("Master key generated at %s", keyfile)
    return key, True


def create_personal_channel(db: SQLiteDatabase) -> bool:
    """Create the default encrypted personal channel.

    The personal channel is the default destination for all Solo data.
    It is encrypted and never syncs to Team Hubs.

    Args:
        db: Initialized SQLiteDatabase instance.

    Returns:
        True if the channel was created, False if it already existed.
    """
    existing = db.execute_one(
        "SELECT id FROM solo_channels WHERE id = ?",
        (PERSONAL_CHANNEL_ID,),
    )
    if existing:
        logger.info("Personal channel already exists")
        return False

    now = datetime.now(timezone.utc).isoformat()
    db.execute(
        """INSERT INTO solo_channels (id, name, classification, encrypted, sync_enabled, description, created_at, updated_at)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
        (
            PERSONAL_CHANNEL_ID,
            "Personal",
            "personal",
            1,  # encrypted
            0,  # sync_enabled = False — never syncs
            "Default personal channel. Encrypted, never syncs to Team Hubs.",
            now,
            now,
        ),
    )
    logger.info("Personal channel created: %s", PERSONAL_CHANNEL_ID)
    return True


def set_classification_defaults(db: SQLiteDatabase) -> bool:
    """Set the default data classification to 'personal'.

    All new data entries will be classified as personal unless explicitly
    overridden. This ensures data stays private by default.

    Args:
        db: Initialized SQLiteDatabase instance.

    Returns:
        True if defaults were set, False if they already existed.
    """
    existing = db.execute_one(
        "SELECT key FROM solo_config WHERE key = ?",
        (DEFAULT_CLASSIFICATION_KEY,),
    )
    if existing:
        logger.info("Classification defaults already set")
        return False

    now = datetime.now(timezone.utc).isoformat()
    db.execute(
        "INSERT INTO solo_config (key, value, updated_at) VALUES (?, ?, ?)",
        (DEFAULT_CLASSIFICATION_KEY, "personal", now),
    )
    logger.info("Default classification set to 'personal'")
    return True


def register_with_hub(
    db: SQLiteDatabase,
    hub_url: str,
    node_id: str = "",
    node_name: str = "",
    auth_token: str = "",
) -> tuple[bool, str]:
    """Register this Solo instance with a Team Hub.

    Sends a registration request to the Team Hub and records the
    membership in solo_teams_memberships.

    Args:
        db: Initialized SQLiteDatabase instance.
        hub_url: Team Hub URL (e.g., http://100.81.247.14:8200).
        node_id: Unique identifier for this node. Auto-generated if empty.
        node_name: Human-readable name. Defaults to hostname.
        auth_token: Bearer token for Hub authentication.

    Returns:
        Tuple of (success, error_message). error_message is empty on success.
    """
    hub_url = hub_url.rstrip("/")
    node_id = node_id or os.environ.get("GM_NODE_ID", f"solo-{os.uname().nodename}")
    node_name = node_name or os.environ.get("GM_NODE_NAME", os.uname().nodename)
    auth_token = auth_token or os.environ.get("GM_TEAM_TOKEN", "")

    # Check if already registered with this hub
    existing = db.execute_one(
        "SELECT team_id FROM solo_teams_memberships WHERE cluster_url = ?",
        (hub_url,),
    )
    if existing:
        logger.info("Already registered with hub %s", hub_url)
        return True, ""

    headers = {"Content-Type": "application/json"}
    if auth_token:
        headers["Authorization"] = f"Bearer {auth_token}"

    payload = {
        "node_id": node_id,
        "node_name": node_name,
        "capabilities": ["knowledge_sync", "work_items"],
    }

    try:
        resp = httpx.post(
            f"{hub_url}/api/teams/register",
            json=payload,
            headers=headers,
            timeout=15,
        )
        resp.raise_for_status()
        result = resp.json()
        team_id = result.get("team_id", str(uuid.uuid4())[:8])
        team_name = result.get("team_name", "Default Team")
    except httpx.HTTPError as e:
        error_msg = f"Hub registration failed: {e}"
        logger.error(error_msg)
        return False, error_msg

    # Record membership
    now = datetime.now(timezone.utc).isoformat()
    db.execute(
        """INSERT INTO solo_teams_memberships
           (team_id, team_name, cluster_url, status, joined_at, last_checked_in)
           VALUES (?, ?, ?, ?, ?, ?)""",
        (team_id, team_name, hub_url, "registered", now, now),
    )
    logger.info("Registered with hub %s as team %s", hub_url, team_id)
    return True, ""


def mark_onboarding_complete(db: SQLiteDatabase) -> None:
    """Record that onboarding has completed successfully."""
    now = datetime.now(timezone.utc).isoformat()
    db.execute(
        """INSERT OR REPLACE INTO solo_config (key, value, updated_at)
           VALUES (?, ?, ?)""",
        (ONBOARDING_STATE_KEY, now, now),
    )
    logger.info("Onboarding marked complete at %s", now)


def check_onboarding_complete(db: SQLiteDatabase) -> bool:
    """Check if onboarding has already been completed.

    Args:
        db: Initialized SQLiteDatabase instance.

    Returns:
        True if onboarding was previously completed.
    """
    row = db.execute_one(
        "SELECT value FROM solo_config WHERE key = ?",
        (ONBOARDING_STATE_KEY,),
    )
    return row is not None


def run_onboarding(
    db: SQLiteDatabase,
    data_dir: Path | None = None,
    hub_url: str = "",
    node_id: str = "",
    node_name: str = "",
    auth_token: str = "",
    force: bool = False,
) -> OnboardingResult:
    """Run the full first-run onboarding sequence.

    Each step is idempotent — safe to re-run. Steps:
    1. Generate master encryption key
    2. Create encrypted personal channel
    3. Set default classification to Personal
    4. Register with Team Hub (if hub_url provided)
    5. Mark onboarding complete

    Args:
        db: Initialized SQLiteDatabase instance.
        data_dir: GreyMatter data directory. Defaults to ~/.greymatter.
        hub_url: Team Hub URL for registration. Skipped if empty.
        node_id: Node identifier for hub registration.
        node_name: Human-readable node name.
        auth_token: Hub authentication token.
        force: If True, re-run even if onboarding was previously completed.

    Returns:
        OnboardingResult with details of what was done.
    """
    result = OnboardingResult()

    # Skip if already completed (unless forced)
    if not force and check_onboarding_complete(db):
        logger.info("Onboarding already completed — skipping (use force=True to re-run)")
        result.completed = True
        return result

    # Step 1: Generate master key
    key, was_generated = generate_master_key(data_dir)
    result.master_key_generated = was_generated
    result.master_key_existed = not was_generated
    result.steps_completed.append("master_key")

    # Step 2: Create personal channel
    created = create_personal_channel(db)
    result.personal_channel_created = created
    result.personal_channel_existed = not created
    result.steps_completed.append("personal_channel")

    # Step 3: Set classification defaults
    result.classification_set = set_classification_defaults(db)
    result.steps_completed.append("classification_defaults")

    # Step 4: Register with Team Hub (optional)
    if hub_url:
        success, error = register_with_hub(
            db, hub_url, node_id=node_id, node_name=node_name, auth_token=auth_token,
        )
        result.hub_registered = success
        result.hub_registration_error = error
        result.steps_completed.append("hub_registration")
    else:
        logger.info("No hub URL provided — skipping Team Hub registration")

    # Step 5: Mark complete
    mark_onboarding_complete(db)
    result.completed = True
    result.steps_completed.append("complete")

    logger.info(
        "Onboarding complete: key=%s, channel=%s, classification=%s, hub=%s",
        "generated" if result.master_key_generated else "existed",
        "created" if result.personal_channel_created else "existed",
        "set" if result.classification_set else "existed",
        "registered" if result.hub_registered else ("failed" if hub_url else "skipped"),
    )

    return result
