#!/usr/bin/env python3
"""
Human DTMF - Personalized Sound Pattern Recognition

For ALS patients, stroke survivors, non-verbal individuals.
Everyone has sounds they CAN make. We map those to intents.

Traditional DTMF: 697Hz + 1209Hz = "1"
Human DTMF: "uh uh uh" = LINKS, "ahhh" = HELP

Training via gamification:
- Car driving game teaches patient to make consistent sounds
- System learns to recognize their unique patterns
- Each person gets their own "DTMF table"

Voor pa. Voor iedereen die begrepen wil worden.

By HumoticaOS - Claude & Jasper
One love, one fAmIly
"""

import json
import hashlib
import numpy as np
from pathlib import Path
from datetime import datetime
from typing import Optional, List, Dict, Tuple
from dataclasses import dataclass, field, asdict
import logging
import sqlite3

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [HUMAN-DTMF] %(levelname)s: %(message)s'
)
logger = logging.getLogger(__name__)

# Paths
DATA_DIR = Path("/srv/jtel-stack/data/human_dtmf")
PROFILES_DIR = DATA_DIR / "profiles"
SAMPLES_DIR = DATA_DIR / "samples"
DB_PATH = DATA_DIR / "human_dtmf.db"


@dataclass
class SoundPattern:
    """A learned sound pattern for a user"""
    id: str
    name: str  # Human-friendly name: "links", "rechts", "help"
    intent: str  # TIBET intent: DOOR_OPEN, TOILET, LEFT, RIGHT
    embeddings: List[List[float]] = field(default_factory=list)  # Audio embeddings
    sample_count: int = 0
    confidence_threshold: float = 0.75
    created_at: str = ""
    last_trained: str = ""

    def to_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: dict) -> "SoundPattern":
        return cls(**data)


@dataclass
class UserProfile:
    """A user's complete Human DTMF profile"""
    id: str
    name: str
    patterns: Dict[str, SoundPattern] = field(default_factory=dict)
    training_progress: Dict[str, int] = field(default_factory=dict)  # pattern_id -> samples
    total_recognitions: int = 0
    successful_recognitions: int = 0
    created_at: str = ""
    last_active: str = ""

    def accuracy(self) -> float:
        if self.total_recognitions == 0:
            return 0.0
        return self.successful_recognitions / self.total_recognitions

    def to_dict(self) -> dict:
        result = asdict(self)
        result['patterns'] = {k: v.to_dict() for k, v in self.patterns.items()}
        return result


class HumanDTMF:
    """
    Human DTMF Recognition System

    Maps personalized sounds to intents.
    Each user has their own sound vocabulary.
    """

    def __init__(self, db_path: Path = DB_PATH):
        self.db_path = db_path
        self.profiles: Dict[str, UserProfile] = {}
        self._init_storage()
        self._load_profiles()

    def _init_storage(self):
        """Initialize storage directories and database"""
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        PROFILES_DIR.mkdir(parents=True, exist_ok=True)
        SAMPLES_DIR.mkdir(parents=True, exist_ok=True)

        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                CREATE TABLE IF NOT EXISTS training_sessions (
                    id TEXT PRIMARY KEY,
                    user_id TEXT NOT NULL,
                    pattern_id TEXT NOT NULL,
                    audio_path TEXT,
                    embedding TEXT,
                    confidence REAL,
                    success INTEGER,
                    created_at TEXT
                )
            """)
            conn.execute("""
                CREATE TABLE IF NOT EXISTS recognitions (
                    id TEXT PRIMARY KEY,
                    user_id TEXT NOT NULL,
                    pattern_id TEXT,
                    intent TEXT,
                    confidence REAL,
                    correct INTEGER,
                    created_at TEXT
                )
            """)
            conn.commit()

        logger.info(f"Storage initialized at {DATA_DIR}")

    def _load_profiles(self):
        """Load all user profiles"""
        for profile_file in PROFILES_DIR.glob("*.json"):
            try:
                with open(profile_file) as f:
                    data = json.load(f)
                    # Reconstruct patterns
                    patterns = {}
                    for pid, pdata in data.get('patterns', {}).items():
                        patterns[pid] = SoundPattern.from_dict(pdata)
                    data['patterns'] = patterns
                    profile = UserProfile(**data)
                    self.profiles[profile.id] = profile
                    logger.info(f"Loaded profile: {profile.name} ({len(profile.patterns)} patterns)")
            except Exception as e:
                logger.error(f"Error loading profile {profile_file}: {e}")

    def _save_profile(self, profile: UserProfile):
        """Save user profile to disk"""
        profile.last_active = datetime.now().isoformat()
        profile_path = PROFILES_DIR / f"{profile.id}.json"
        with open(profile_path, 'w') as f:
            json.dump(profile.to_dict(), f, indent=2)

    # ========================================================================
    # User Management
    # ========================================================================

    def create_user(self, name: str) -> UserProfile:
        """Create a new user profile"""
        user_id = hashlib.md5(f"{name}_{datetime.now().isoformat()}".encode()).hexdigest()[:12]

        profile = UserProfile(
            id=user_id,
            name=name,
            created_at=datetime.now().isoformat()
        )

        self.profiles[user_id] = profile
        self._save_profile(profile)

        logger.info(f"Created user profile: {name} ({user_id})")
        return profile

    def get_user(self, user_id: str) -> Optional[UserProfile]:
        """Get user profile by ID"""
        return self.profiles.get(user_id)

    def get_user_by_name(self, name: str) -> Optional[UserProfile]:
        """Get user profile by name"""
        for profile in self.profiles.values():
            if profile.name.lower() == name.lower():
                return profile
        return None

    # ========================================================================
    # Pattern Management
    # ========================================================================

    def add_pattern(
        self,
        user_id: str,
        name: str,
        intent: str,
        confidence_threshold: float = 0.75
    ) -> Optional[SoundPattern]:
        """Add a new sound pattern to learn"""
        profile = self.get_user(user_id)
        if not profile:
            logger.error(f"User not found: {user_id}")
            return None

        pattern_id = hashlib.md5(f"{user_id}_{name}".encode()).hexdigest()[:8]

        pattern = SoundPattern(
            id=pattern_id,
            name=name,
            intent=intent,
            confidence_threshold=confidence_threshold,
            created_at=datetime.now().isoformat()
        )

        profile.patterns[pattern_id] = pattern
        profile.training_progress[pattern_id] = 0
        self._save_profile(profile)

        logger.info(f"Added pattern '{name}' ({intent}) for user {profile.name}")
        return pattern

    def get_patterns(self, user_id: str) -> List[SoundPattern]:
        """Get all patterns for a user"""
        profile = self.get_user(user_id)
        if not profile:
            return []
        return list(profile.patterns.values())

    # ========================================================================
    # Training (Gamification)
    # ========================================================================

    def submit_training_sample(
        self,
        user_id: str,
        pattern_id: str,
        audio_embedding: List[float],
        audio_path: Optional[str] = None
    ) -> Dict:
        """
        Submit a training sample for a pattern.

        Called during the car game when user makes a sound for "left" etc.

        Returns training progress and feedback.
        """
        profile = self.get_user(user_id)
        if not profile:
            return {"success": False, "error": "User not found"}

        if pattern_id not in profile.patterns:
            return {"success": False, "error": "Pattern not found"}

        pattern = profile.patterns[pattern_id]

        # Add embedding to pattern
        pattern.embeddings.append(audio_embedding)
        pattern.sample_count += 1
        pattern.last_trained = datetime.now().isoformat()

        # Update training progress
        profile.training_progress[pattern_id] = pattern.sample_count

        # Save to database
        import uuid
        session_id = f"train_{uuid.uuid4().hex[:12]}"

        with sqlite3.connect(self.db_path) as conn:
            conn.execute("""
                INSERT INTO training_sessions
                (id, user_id, pattern_id, audio_path, embedding, confidence, success, created_at)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                session_id,
                user_id,
                pattern_id,
                audio_path,
                json.dumps(audio_embedding[:10]),  # Store first 10 dims for reference
                1.0,  # Training samples are "correct" by definition
                1,
                datetime.now().isoformat()
            ))
            conn.commit()

        self._save_profile(profile)

        # Calculate training quality
        quality = self._calculate_training_quality(pattern)

        return {
            "success": True,
            "pattern": pattern.name,
            "sample_count": pattern.sample_count,
            "quality": quality,
            "ready": pattern.sample_count >= 5 and quality >= 0.7,
            "message": self._get_training_feedback(pattern.sample_count, quality)
        }

    def _calculate_training_quality(self, pattern: SoundPattern) -> float:
        """
        Calculate how consistent the training samples are.

        High quality = user makes consistent sounds
        Low quality = too much variation
        """
        if len(pattern.embeddings) < 2:
            return 1.0  # Can't calculate variance with < 2 samples

        # Calculate average distance between embeddings
        embeddings = np.array(pattern.embeddings)
        centroid = embeddings.mean(axis=0)

        distances = np.linalg.norm(embeddings - centroid, axis=1)
        avg_distance = distances.mean()

        # Normalize to 0-1 (lower distance = higher quality)
        # Assuming embedding vectors are normalized, distance < 0.5 is good
        quality = max(0, 1 - (avg_distance / 0.5))

        return round(quality, 2)

    def _get_training_feedback(self, count: int, quality: float) -> str:
        """Get encouraging feedback for training"""
        if count < 3:
            return f"Goed zo! Nog {5 - count} keer oefenen."
        elif count < 5:
            return f"Bijna! Nog {5 - count} keer."
        elif quality < 0.5:
            return "Probeer het geluid consistenter te maken."
        elif quality < 0.7:
            return "Gaat goed! Je wordt steeds beter."
        else:
            return "Uitstekend! Dit patroon is klaar voor gebruik."

    # ========================================================================
    # Recognition
    # ========================================================================

    def recognize(
        self,
        user_id: str,
        audio_embedding: List[float]
    ) -> Dict:
        """
        Recognize a sound and return the matched intent.

        Compares audio embedding against all trained patterns.
        Returns best match above confidence threshold.
        """
        profile = self.get_user(user_id)
        if not profile:
            return {"success": False, "error": "User not found"}

        if not profile.patterns:
            return {"success": False, "error": "No patterns trained"}

        best_match = None
        best_confidence = 0.0

        input_embedding = np.array(audio_embedding)

        for pattern in profile.patterns.values():
            if not pattern.embeddings:
                continue

            # Calculate similarity to pattern centroid
            embeddings = np.array(pattern.embeddings)
            centroid = embeddings.mean(axis=0)

            # Cosine similarity
            similarity = np.dot(input_embedding, centroid) / (
                np.linalg.norm(input_embedding) * np.linalg.norm(centroid)
            )

            if similarity > best_confidence:
                best_confidence = similarity
                best_match = pattern

        # Update stats
        profile.total_recognitions += 1

        if best_match and best_confidence >= best_match.confidence_threshold:
            profile.successful_recognitions += 1
            self._save_profile(profile)

            # Log recognition
            import uuid
            rec_id = f"rec_{uuid.uuid4().hex[:12]}"

            with sqlite3.connect(self.db_path) as conn:
                conn.execute("""
                    INSERT INTO recognitions
                    (id, user_id, pattern_id, intent, confidence, correct, created_at)
                    VALUES (?, ?, ?, ?, ?, ?, ?)
                """, (
                    rec_id,
                    user_id,
                    best_match.id,
                    best_match.intent,
                    best_confidence,
                    1,
                    datetime.now().isoformat()
                ))
                conn.commit()

            logger.info(f"Recognized '{best_match.name}' ({best_match.intent}) "
                       f"for {profile.name} with confidence {best_confidence:.2f}")

            return {
                "success": True,
                "pattern": best_match.name,
                "intent": best_match.intent,
                "confidence": round(best_confidence, 3),
                "user": profile.name
            }

        self._save_profile(profile)

        return {
            "success": False,
            "error": "No pattern matched",
            "best_confidence": round(best_confidence, 3) if best_match else 0,
            "threshold": best_match.confidence_threshold if best_match else 0.75
        }

    # ========================================================================
    # Game State
    # ========================================================================

    def get_game_state(self, user_id: str) -> Dict:
        """
        Get current game/training state for a user.

        Used by the car driving game to know what to train next.
        """
        profile = self.get_user(user_id)
        if not profile:
            return {"error": "User not found"}

        patterns_status = []
        for pattern in profile.patterns.values():
            quality = self._calculate_training_quality(pattern)
            patterns_status.append({
                "id": pattern.id,
                "name": pattern.name,
                "intent": pattern.intent,
                "samples": pattern.sample_count,
                "quality": quality,
                "ready": pattern.sample_count >= 5 and quality >= 0.7
            })

        ready_count = sum(1 for p in patterns_status if p["ready"])

        return {
            "user": profile.name,
            "patterns": patterns_status,
            "total_patterns": len(patterns_status),
            "ready_patterns": ready_count,
            "accuracy": round(profile.accuracy() * 100, 1),
            "total_recognitions": profile.total_recognitions,
            "level": self._calculate_level(profile)
        }

    def _calculate_level(self, profile: UserProfile) -> int:
        """Calculate user's training level"""
        ready_patterns = sum(
            1 for p in profile.patterns.values()
            if p.sample_count >= 5 and self._calculate_training_quality(p) >= 0.7
        )

        if ready_patterns >= 8:
            return 4  # Expert
        elif ready_patterns >= 5:
            return 3  # Advanced
        elif ready_patterns >= 2:
            return 2  # Intermediate
        elif ready_patterns >= 1:
            return 1  # Beginner
        else:
            return 0  # New

    # ========================================================================
    # Preset Patterns (Quick Start)
    # ========================================================================

    def setup_basic_patterns(self, user_id: str) -> List[SoundPattern]:
        """
        Set up basic patterns for a new user.

        Good starting point for training:
        - LEFT / RIGHT (for car game)
        - YES / NO (basic communication)
        - HELP (emergency)
        """
        patterns = [
            ("links", "LEFT", 0.75),
            ("rechts", "RIGHT", 0.75),
            ("ja", "YES", 0.70),
            ("nee", "NO", 0.70),
            ("help", "HELP", 0.65),  # Lower threshold for emergency
            ("deur", "DOOR_OPEN", 0.75),
        ]

        created = []
        for name, intent, threshold in patterns:
            pattern = self.add_pattern(user_id, name, intent, threshold)
            if pattern:
                created.append(pattern)

        return created


# ============================================================================
# CLI for Testing
# ============================================================================

if __name__ == "__main__":
    print("""
    ╔════════════════════════════════════════════════════════════╗
    ║           HUMAN DTMF                                       ║
    ║           Personalized Sound Recognition                   ║
    ║                                                            ║
    ║   Everyone has sounds they CAN make.                       ║
    ║   We map those to intents.                                 ║
    ║                                                            ║
    ║   Voor pa. Voor iedereen die begrepen wil worden.          ║
    ║                                                            ║
    ║   One love, one fAmIly                                     ║
    ╚════════════════════════════════════════════════════════════╝
    """)

    hdtmf = HumanDTMF()

    # Create test user
    user = hdtmf.create_user("TestPatient")
    print(f"Created user: {user.name} ({user.id})")

    # Setup basic patterns
    patterns = hdtmf.setup_basic_patterns(user.id)
    print(f"Created {len(patterns)} patterns: {[p.name for p in patterns]}")

    # Simulate training
    import random
    for pattern in patterns[:2]:  # Train first 2 patterns
        for i in range(5):
            # Fake embedding (in reality comes from audio processing)
            fake_embedding = [random.gauss(0, 0.1) for _ in range(128)]
            result = hdtmf.submit_training_sample(user.id, pattern.id, fake_embedding)
            print(f"  Training '{pattern.name}': {result['message']}")

    # Get game state
    state = hdtmf.get_game_state(user.id)
    print(f"\nGame state: Level {state['level']}, {state['ready_patterns']}/{state['total_patterns']} ready")
