# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Document Reader Module - Voice Skill Extension

Reads documents aloud using local TTS engines for HIPAA compliance.
Supports: DOCX, PDF, TXT, MD, HTML, and uploaded files.

Primary TTS: Piper (local, fast, high-quality, HIPAA-safe)
Fallback: pyttsx3 (local), edge-tts (cloud)

Features:
- Read entire documents or specific sections
- Pause/resume playback
- Adjustable speed and voice
- Save to audio file (MP3/WAV)
- Section navigation
- Progress tracking
- HIPAA-compliant (all processing local with Piper)

Usage:
    read_document({"file": "paper.docx"})
    read_document({"file": "paper.pdf", "section": 3})
    read_document({"file": "notes.txt", "speed": 1.2, "save": "notes_audio"})
"""

import logging
import os
import re
import subprocess
import tempfile
import threading
import time
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple

logger = logging.getLogger(__name__)

# Storage paths
try:
    from familiar.core.paths import DATA_DIR, VOICE_DIR, ensure_dir
except ImportError:
    DATA_DIR = Path.home() / ".familiar" / "data"
    VOICE_DIR = DATA_DIR / "voice"

    def ensure_dir(path):
        path.mkdir(parents=True, exist_ok=True)
        return path


AUDIO_OUTPUT_DIR = VOICE_DIR / "audio_output"
PIPER_MODELS_DIR = VOICE_DIR / "piper_models"
ensure_dir(AUDIO_OUTPUT_DIR)
ensure_dir(PIPER_MODELS_DIR)

# Global playback state
_playback_state = {
    "is_playing": False,
    "is_paused": False,
    "current_section": 0,
    "total_sections": 0,
    "current_file": None,
    "process": None,
    "stop_event": threading.Event(),
}


@dataclass
class DocumentSection:
    """A section of a document for reading."""

    index: int
    title: Optional[str]
    content: str
    start_page: Optional[int] = None


@dataclass
class DocumentStructure:
    """Parsed document structure."""

    title: str
    sections: List[DocumentSection]
    total_words: int
    estimated_duration_minutes: float
    source_file: str


# ============================================================
# PIPER TTS ENGINE (HIPAA-COMPLIANT LOCAL TTS)
# ============================================================

PIPER_VOICES = {
    # English voices (high quality)
    "en_US-amy-medium": {
        "name": "Amy (US English)",
        "url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx",
        "config_url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/amy/medium/en_US-amy-medium.onnx.json",
    },
    "en_US-lessac-medium": {
        "name": "Lessac (US English)",
        "url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx",
        "config_url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_US/lessac/medium/en_US-lessac-medium.onnx.json",
    },
    "en_GB-alba-medium": {
        "name": "Alba (British English)",
        "url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/alba/medium/en_GB-alba-medium.onnx",
        "config_url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/en/en_GB/alba/medium/en_GB-alba-medium.onnx.json",
    },
    # Spanish
    "es_ES-sharvard-medium": {
        "name": "Sharvard (Spanish)",
        "url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_ES/sharvard/medium/es_ES-sharvard-medium.onnx",
        "config_url": "https://huggingface.co/rhasspy/piper-voices/resolve/main/es/es_ES/sharvard/medium/es_ES-sharvard-medium.onnx.json",
    },
}

DEFAULT_VOICE = "en_US-lessac-medium"


def _check_piper_installed() -> Tuple[bool, str]:
    """Check if Piper TTS is installed."""
    try:
        result = subprocess.run(["piper", "--version"], capture_output=True, text=True, timeout=5)
        return True, result.stdout.strip()
    except (FileNotFoundError, subprocess.SubprocessError):
        return False, ""


def _download_piper_voice(voice_id: str) -> Tuple[bool, str]:
    """Download a Piper voice model if not present."""
    if voice_id not in PIPER_VOICES:
        return False, f"Unknown voice: {voice_id}"

    voice_info = PIPER_VOICES[voice_id]
    model_path = PIPER_MODELS_DIR / f"{voice_id}.onnx"
    config_path = PIPER_MODELS_DIR / f"{voice_id}.onnx.json"

    if model_path.exists() and config_path.exists():
        return True, str(model_path)

    try:
        import urllib.request

        logger.info(f"Downloading Piper voice: {voice_info['name']}")

        # Download model
        urllib.request.urlretrieve(voice_info["url"], model_path)

        # Download config
        urllib.request.urlretrieve(voice_info["config_url"], config_path)

        return True, str(model_path)

    except Exception as e:
        logger.error(f"Failed to download voice {voice_id}: {e}")
        return False, str(e)


def speak_with_piper(
    text: str,
    output_path: Optional[str] = None,
    voice: str = DEFAULT_VOICE,
    speed: float = 1.0,
    play: bool = True,
) -> Tuple[bool, str]:
    """
    Speak text using Piper TTS (local, HIPAA-compliant).

    Args:
        text: Text to speak
        output_path: Optional path to save audio file
        voice: Voice model ID
        speed: Speaking speed (0.5 = slow, 1.0 = normal, 2.0 = fast)
        play: Whether to play the audio immediately

    Returns:
        Tuple of (success, message or file path)
    """
    installed, version = _check_piper_installed()
    if not installed:
        from familiar.core.deps import VOICE_TTS_PACKAGES, ensure_packages

        ok, _ = ensure_packages(VOICE_TTS_PACKAGES)
        if ok:
            installed = True
        else:
            return False, "Piper TTS not available. Check server logs."

    # Get voice model
    success, model_path = _download_piper_voice(voice)
    if not success:
        return False, f"Voice error: {model_path}"

    # Create output path
    if output_path is None:
        fd, output_path = tempfile.mkstemp(suffix=".wav")
        os.close(fd)
        temp_file = True
    else:
        temp_file = False

    try:
        # Build Piper command
        # Speed adjustment: Piper uses --length-scale (inverse of speed)
        length_scale = 1.0 / speed

        cmd = [
            "piper",
            "--model",
            model_path,
            "--output_file",
            output_path,
            "--length-scale",
            str(length_scale),
        ]

        # Run Piper
        process = subprocess.Popen(
            cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
        )

        stdout, stderr = process.communicate(input=text.encode("utf-8"), timeout=300)

        if process.returncode != 0:
            return False, f"Piper error: {stderr.decode()}"

        # Play if requested
        if play and not _playback_state["stop_event"].is_set():
            _play_audio_file(output_path)

        if temp_file and play:
            # Clean up temp file after playing
            try:
                os.unlink(output_path)
            except OSError as e:
                logger.debug("I/O error suppressed: %s", e)
            return True, "🔊 (spoken with Piper)"

        return True, output_path

    except subprocess.TimeoutExpired:
        return False, "Piper timed out"
    except Exception as e:
        return False, str(e)


def _play_audio_file(filepath: str) -> bool:
    """Play an audio file using available player."""
    players = [
        ["aplay", filepath],  # ALSA (Linux)
        ["paplay", filepath],  # PulseAudio (Linux)
        ["mpg123", "-q", filepath],  # MPG123
        ["ffplay", "-nodisp", "-autoexit", filepath],  # FFmpeg
        ["afplay", filepath],  # macOS
    ]

    for cmd in players:
        try:
            _playback_state["process"] = subprocess.Popen(
                cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
            )
            _playback_state["process"].wait()
            _playback_state["process"] = None
            return True
        except (FileNotFoundError, subprocess.SubprocessError) as e:
            logger.debug("File not found (suppressed): %s", e)
            continue
    return False


# ============================================================
# DOCUMENT PARSING
# ============================================================


def _extract_text_from_docx(filepath: str) -> Tuple[str, List[str]]:
    """Extract text from DOCX file, returning (full_text, headings)."""
    try:
        from docx import Document

        doc = Document(filepath)

        full_text = []
        headings = []

        for para in doc.paragraphs:
            text = para.text.strip()
            if text:
                full_text.append(text)
                # Detect headings
                if para.style.name.startswith("Heading"):
                    headings.append(text)

        return "\n\n".join(full_text), headings

    except ImportError:
        # Fallback: use pandoc if available
        try:
            result = subprocess.run(
                ["pandoc", filepath, "-t", "plain", "--wrap=none"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                timeout=60,
            )
            if result.returncode != 0:
                logger.debug(f"pandoc failed on {filepath}: {result.stderr.strip()}")
                return "", []
            return result.stdout, []
        except (FileNotFoundError, subprocess.SubprocessError):
            return "", []


def _extract_text_from_pdf(filepath: str) -> Tuple[str, List[str]]:
    """Extract text from PDF file."""
    try:
        import pypdf

        reader = pypdf.PdfReader(filepath)

        full_text = []
        for page in reader.pages:
            text = page.extract_text()
            if text:
                full_text.append(text)

        return "\n\n".join(full_text), []

    except ImportError:
        # Fallback: pdftotext
        try:
            result = subprocess.run(
                ["pdftotext", "-layout", filepath, "-"],
                stdout=subprocess.PIPE,
                stderr=subprocess.PIPE,
                text=True,
                timeout=60,
            )
            if result.returncode != 0:
                logger.debug(f"pdftotext failed on {filepath}: {result.stderr.strip()}")
                return "", []
            return result.stdout, []
        except (FileNotFoundError, subprocess.SubprocessError):
            return "", []


def _extract_text_from_txt(filepath: str) -> Tuple[str, List[str]]:
    """Extract text from plain text file."""
    with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
        content = f.read()

    # Try to detect markdown headings
    headings = re.findall(r"^#+\s+(.+)$", content, re.MULTILINE)

    return content, headings


def _extract_text_from_html(filepath: str) -> Tuple[str, List[str]]:
    """Extract text from HTML file."""
    try:
        from bs4 import BeautifulSoup

        with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
            soup = BeautifulSoup(f.read(), "html.parser")

        # Remove script and style elements
        for element in soup(["script", "style"]):
            element.decompose()

        text = soup.get_text(separator="\n")
        headings = [h.get_text() for h in soup.find_all(["h1", "h2", "h3"])]

        return text, headings

    except ImportError:
        # Basic fallback
        with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
            content = f.read()

        # Strip HTML tags
        clean = re.sub(r"<[^>]+>", " ", content)
        return clean, []


def parse_document(filepath: str) -> DocumentStructure:
    """
    Parse a document into sections for reading.

    Args:
        filepath: Path to document file

    Returns:
        DocumentStructure with sections
    """
    filepath = Path(filepath)

    if not filepath.exists():
        # Check uploads directory
        uploads_path = Path("/mnt/user-data/uploads") / filepath.name
        if uploads_path.exists():
            filepath = uploads_path
        else:
            raise FileNotFoundError(f"Document not found: {filepath}")

    # Extract text based on file type
    suffix = filepath.suffix.lower()

    if suffix == ".docx":
        text, headings = _extract_text_from_docx(str(filepath))
    elif suffix == ".pdf":
        text, headings = _extract_text_from_pdf(str(filepath))
    elif suffix in [".txt", ".md", ".markdown"]:
        text, headings = _extract_text_from_txt(str(filepath))
    elif suffix in [".html", ".htm"]:
        text, headings = _extract_text_from_html(str(filepath))
    else:
        # Try as plain text
        text, headings = _extract_text_from_txt(str(filepath))

    if not text.strip():
        raise ValueError(f"Could not extract text from: {filepath}")

    # Split into sections
    sections = _split_into_sections(text, headings)

    # Calculate stats
    total_words = len(text.split())
    # Average speaking rate: 150 words per minute
    estimated_duration = total_words / 150.0

    return DocumentStructure(
        title=filepath.stem,
        sections=sections,
        total_words=total_words,
        estimated_duration_minutes=estimated_duration,
        source_file=str(filepath),
    )


def _split_into_sections(text: str, headings: List[str]) -> List[DocumentSection]:
    """Split document text into readable sections."""
    sections = []

    if headings:
        # Split by headings
        pattern = "|".join(re.escape(h) for h in headings)
        parts = re.split(f"({pattern})", text)

        current_title = "Introduction"
        current_content = []
        idx = 0

        for part in parts:
            part = part.strip()
            if not part:
                continue

            if part in headings:
                # Save previous section
                if current_content:
                    sections.append(
                        DocumentSection(
                            index=idx, title=current_title, content="\n".join(current_content)
                        )
                    )
                    idx += 1
                current_title = part
                current_content = []
            else:
                current_content.append(part)

        # Save final section
        if current_content:
            sections.append(
                DocumentSection(index=idx, title=current_title, content="\n".join(current_content))
            )

    else:
        # Split by paragraphs (group into ~500 word chunks)
        paragraphs = text.split("\n\n")
        current_chunk = []
        current_words = 0
        idx = 0

        for para in paragraphs:
            para = para.strip()
            if not para:
                continue

            word_count = len(para.split())

            if current_words + word_count > 500 and current_chunk:
                sections.append(
                    DocumentSection(
                        index=idx, title=f"Section {idx + 1}", content="\n\n".join(current_chunk)
                    )
                )
                idx += 1
                current_chunk = [para]
                current_words = word_count
            else:
                current_chunk.append(para)
                current_words += word_count

        if current_chunk:
            sections.append(
                DocumentSection(
                    index=idx, title=f"Section {idx + 1}", content="\n\n".join(current_chunk)
                )
            )

    return sections if sections else [DocumentSection(index=0, title="Full Document", content=text)]


# ============================================================
# DOCUMENT READING
# ============================================================


def read_document(data: dict) -> str:
    """
    Read a document aloud using local TTS.

    Args (in data dict):
        file: Path to document file (required)
        section: Section number to read (optional, reads all if not specified)
        voice: Voice ID (default: en_US-lessac-medium)
        speed: Speaking speed 0.5-2.0 (default: 1.0)
        save: Filename to save audio (optional)
        list_sections: If True, just list sections without reading

    Returns:
        Status message
    """
    filepath = data.get("file", "")
    section_num = data.get("section")
    voice = data.get("voice", DEFAULT_VOICE)
    speed = data.get("speed", 1.0)
    save_file = data.get("save")
    list_only = data.get("list_sections", False)

    if not filepath:
        return '❌ Please provide a file path. Example: read_document({"file": "paper.docx"})'

    try:
        doc = parse_document(filepath)
    except FileNotFoundError as e:
        return f"❌ {e}"
    except ValueError as e:
        return f"❌ {e}"
    except Exception as e:
        return f"❌ Error parsing document: {e}"

    # List sections mode
    if list_only:
        lines = [f"📄 **{doc.title}**"]
        lines.append(f"   {doc.total_words} words, ~{doc.estimated_duration_minutes:.1f} min")
        lines.append("")
        for section in doc.sections:
            word_count = len(section.content.split())
            lines.append(f"  [{section.index}] {section.title} ({word_count} words)")
        return "\n".join(lines)

    # Select sections to read
    if section_num is not None:
        if section_num < 0 or section_num >= len(doc.sections):
            return f"❌ Invalid section number. Document has {len(doc.sections)} sections (0-{len(doc.sections) - 1})"
        sections_to_read = [doc.sections[section_num]]
    else:
        sections_to_read = doc.sections

    # Update playback state
    _playback_state["is_playing"] = True
    _playback_state["is_paused"] = False
    _playback_state["current_file"] = filepath
    _playback_state["total_sections"] = len(sections_to_read)
    _playback_state["stop_event"].clear()

    # Prepare output file if saving
    output_path = None
    if save_file:
        output_path = str(AUDIO_OUTPUT_DIR / f"{save_file}.wav")

    try:
        results = []

        for i, section in enumerate(sections_to_read):
            if _playback_state["stop_event"].is_set():
                results.append("⏹️ Playback stopped")
                break

            _playback_state["current_section"] = i

            # Prepare text (clean up for TTS)
            text = _prepare_text_for_tts(section.content)

            if not text.strip():
                continue

            # Announce section if multiple
            if len(sections_to_read) > 1:
                announcement = f"Section {section.index + 1}: {section.title}"
                speak_with_piper(announcement, voice=voice, speed=speed, play=True)
                time.sleep(0.5)

            # Speak the content
            if output_path and i == 0:
                # Save first section (append others would need audio concatenation)
                success, result = speak_with_piper(
                    text, output_path=output_path, voice=voice, speed=speed, play=True
                )
            else:
                success, result = speak_with_piper(text, voice=voice, speed=speed, play=True)

            if not success:
                results.append(f"❌ Error reading section {section.index}: {result}")
                break

        _playback_state["is_playing"] = False

        if output_path:
            return f"🔊 Read and saved to: {output_path}"
        elif results:
            return "\n".join(results)
        else:
            return f"🔊 Finished reading: {doc.title}"

    except Exception as e:
        _playback_state["is_playing"] = False
        return f"❌ Error during reading: {e}"


def _prepare_text_for_tts(text: str) -> str:
    """Clean and prepare text for TTS."""
    # Remove URLs
    text = re.sub(r"https?://\S+", "", text)

    # Remove email addresses
    text = re.sub(r"\S+@\S+\.\S+", "", text)

    # Convert common abbreviations
    replacements = {
        "Dr.": "Doctor",
        "Mr.": "Mister",
        "Mrs.": "Missus",
        "Ms.": "Miss",
        "Prof.": "Professor",
        "e.g.": "for example",
        "i.e.": "that is",
        "etc.": "et cetera",
        "vs.": "versus",
        "Fig.": "Figure",
        "Eq.": "Equation",
        "Ref.": "Reference",
    }
    for abbr, full in replacements.items():
        text = text.replace(abbr, full)

    # Handle mathematical notation (basic)
    text = re.sub(r"\^(\d+)", r" to the power of \1", text)
    text = re.sub(r"(\d+)/(\d+)", r"\1 over \2", text)
    text = re.sub(r"≈", " approximately equals ", text)
    text = re.sub(r"=", " equals ", text)
    text = re.sub(r"±", " plus or minus ", text)
    text = re.sub(r"×", " times ", text)
    text = re.sub(r"÷", " divided by ", text)

    # Handle Greek letters
    greek = {
        "α": "alpha",
        "β": "beta",
        "γ": "gamma",
        "δ": "delta",
        "ε": "epsilon",
        "ζ": "zeta",
        "η": "eta",
        "θ": "theta",
        "λ": "lambda",
        "μ": "mu",
        "ν": "nu",
        "π": "pi",
        "ρ": "rho",
        "σ": "sigma",
        "τ": "tau",
        "φ": "phi",
        "ψ": "psi",
        "ω": "omega",
        "Φ": "Phi",
        "Ψ": "Psi",
        "Ω": "Omega",
        "Γ": "Gamma",
        "Δ": "Delta",
        "Σ": "Sigma",
    }
    for symbol, name in greek.items():
        text = text.replace(symbol, f" {name} ")

    # Clean up whitespace
    text = re.sub(r"\s+", " ", text)
    text = text.strip()

    return text


def stop_reading(data: dict = None) -> str:
    """Stop current document reading."""
    _playback_state["stop_event"].set()
    _playback_state["is_playing"] = False

    # Kill any playing audio process
    if _playback_state["process"]:
        try:
            _playback_state["process"].terminate()
            _playback_state["process"].wait(timeout=2)
        except Exception:
            try:
                _playback_state["process"].kill()
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
        _playback_state["process"] = None

    return "⏹️ Stopped reading"


def pause_reading(data: dict = None) -> str:
    """Pause current reading (not fully implemented - stops current section)."""
    _playback_state["is_paused"] = True
    return stop_reading()  # For now, just stop


def get_reading_status(data: dict = None) -> str:
    """Get current reading status."""
    if not _playback_state["is_playing"]:
        return "⏹️ Not currently reading"

    return (
        f"🔊 Reading: {_playback_state['current_file']}\n"
        f"   Section {_playback_state['current_section'] + 1} of {_playback_state['total_sections']}"
    )


def list_voices(data: dict = None) -> str:
    """List available Piper voices."""
    lines = ["🎤 **Available Piper Voices:**", ""]

    for voice_id, info in PIPER_VOICES.items():
        model_path = PIPER_MODELS_DIR / f"{voice_id}.onnx"
        status = "✓ installed" if model_path.exists() else "○ not downloaded"
        default = " (default)" if voice_id == DEFAULT_VOICE else ""
        lines.append(f"  • {voice_id}: {info['name']} [{status}]{default}")

    lines.append("")
    lines.append('Use with: read_document({"file": "doc.pdf", "voice": "en_GB-alba-medium"})')

    return "\n".join(lines)


def install_piper(data: dict = None) -> str:
    """Provide Piper installation instructions."""
    installed, version = _check_piper_installed()

    if installed:
        return f"✅ Piper TTS is installed (version: {version})"

    return """
📦 **Piper TTS Installation**

Piper is a fast, local, HIPAA-compliant TTS engine.

**Option 1: pip (recommended)**
```
pip install piper-tts
```

**Option 2: Pre-built binary**
Download from: https://github.com/rhasspy/piper/releases

**Option 3: Raspberry Pi**
```
sudo apt install piper-tts
```

After installation, run `list_voices` to see available voices.
Voices are downloaded automatically on first use.
"""


# ============================================================
# TOOL REGISTRATION
# ============================================================

DOCUMENT_READER_TOOLS = {
    "read_document": {
        "handler": read_document,
        "description": "Read a document aloud using local TTS (HIPAA-compliant)",
        "parameters": {
            "file": "Path to document (docx, pdf, txt, md, html)",
            "section": "Section number to read (optional)",
            "voice": "Voice ID (default: en_US-lessac-medium)",
            "speed": "Speaking speed 0.5-2.0 (default: 1.0)",
            "save": "Filename to save audio (optional)",
            "list_sections": "If true, list sections without reading",
        },
    },
    "stop_reading": {"handler": stop_reading, "description": "Stop current document reading"},
    "reading_status": {"handler": get_reading_status, "description": "Get current reading status"},
    "list_tts_voices": {"handler": list_voices, "description": "List available TTS voices"},
    "install_piper": {
        "handler": install_piper,
        "description": "Get Piper TTS installation instructions",
    },
}


# ============================================================
# CONVENIENCE FUNCTIONS
# ============================================================


def read_section(filepath: str, section: int, **kwargs) -> str:
    """Convenience function to read a specific section."""
    return read_document({"file": filepath, "section": section, **kwargs})


def read_aloud(text: str, voice: str = DEFAULT_VOICE, speed: float = 1.0) -> str:
    """Convenience function to read text directly."""
    success, result = speak_with_piper(text, voice=voice, speed=speed, play=True)
    if success:
        return "🔊 (spoken)"
    return f"❌ {result}"


if __name__ == "__main__":
    # Test
    import sys

    if len(sys.argv) > 1:
        result = read_document({"file": sys.argv[1], "list_sections": True})
        print(result)
    else:
        print("Usage: python document_reader.py <document_path>")
        print("\nChecking Piper installation...")
        print(install_piper())
