"""Core memory commands: remember, todo, recall, context."""

from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING, Annotated, Any

if TYPE_CHECKING:
    from neural_memory.cli.storage import PersistentStorage

import typer

from neural_memory.cli._helpers import get_config, get_storage, output_result, run_async
from neural_memory.core.memory_types import (
    DEFAULT_EXPIRY_DAYS,
    MemoryType,
    Priority,
    TypedMemory,
    suggest_memory_type,
)
from neural_memory.engine.encoder import MemoryEncoder
from neural_memory.engine.retrieval import DepthLevel, ReflexPipeline
from neural_memory.extraction.parser import QueryParser
from neural_memory.extraction.router import QueryRouter
from neural_memory.safety.freshness import (
    FreshnessLevel,
    analyze_freshness,
    evaluate_freshness,
    format_age,
    get_freshness_indicator,
)
from neural_memory.safety.sensitive import (
    check_sensitive_content,
    filter_sensitive_content,
    format_sensitive_warning,
)
from neural_memory.utils.timeutils import utcnow


def _validate_content(
    content: str,
    *,
    force: bool,
    redact: bool,
) -> tuple[str, list[Any]]:
    """Validate and optionally redact sensitive content. Returns (content, matches)."""
    sensitive_matches = check_sensitive_content(content, min_severity=2)

    if sensitive_matches and not force and not redact:
        warning = format_sensitive_warning(sensitive_matches)
        typer.echo(warning)
        raise typer.Exit(1)

    store_content = content
    if redact and sensitive_matches:
        store_content, _ = filter_sensitive_content(content)
        typer.secho(f"Redacted {len(sensitive_matches)} sensitive item(s)", fg=typer.colors.YELLOW)

    return store_content, sensitive_matches


def _resolve_memory_type(
    memory_type: str | None,
    content: str,
) -> MemoryType:
    """Parse explicit memory type or auto-detect."""
    if memory_type:
        try:
            return MemoryType(memory_type.lower())
        except ValueError:
            valid_types = ", ".join(t.value for t in MemoryType)
            typer.secho(f"Invalid memory type. Valid types: {valid_types}", fg=typer.colors.RED)
            raise typer.Exit(1)
    return suggest_memory_type(content)


async def _resolve_project_id(storage: PersistentStorage, project: str | None) -> str | None:
    """Look up project by name, return ID or None."""
    if not project:
        return None
    proj = await storage.get_project_by_name(project)
    if not proj:
        return None
    return proj.id


async def _encode_and_store(
    storage: PersistentStorage,
    brain_config: Any,
    content: str,
    *,
    tags: set[str] | None,
    mem_type: MemoryType,
    mem_priority: Priority,
    expiry_days: int | None,
    project_id: str | None,
    event_timestamp: datetime | None = None,
) -> dict[str, Any]:
    """Encode content into neural graph and store typed memory metadata."""
    encoder = MemoryEncoder(storage, brain_config)
    storage.disable_auto_save()

    result = await encoder.encode(
        content=content,
        timestamp=event_timestamp or utcnow(),
        tags=tags,
    )

    typed_mem = TypedMemory.create(
        fiber_id=result.fiber.id,
        memory_type=mem_type,
        priority=mem_priority,
        source="user_input",
        expires_in_days=expiry_days,
        tags=tags,
        project_id=project_id,
    )
    await storage.add_typed_memory(typed_mem)
    await storage.batch_save()

    return {
        "fiber_id": result.fiber.id,
        "typed_mem": typed_mem,
        "neurons_created": len(result.neurons_created),
        "neurons_linked": len(result.neurons_linked),
        "synapses_created": len(result.synapses_created),
    }


def remember(
    content: Annotated[str, typer.Argument(help="Content to remember")] = "",
    tags: Annotated[
        list[str] | None, typer.Option("--tag", "-t", help="Tags for the memory")
    ] = None,
    memory_type: Annotated[
        str | None,
        typer.Option(
            "--type",
            "-T",
            help="Memory type: fact, decision, preference, todo, insight, context, instruction, error, workflow, reference (auto-detected if not specified)",
        ),
    ] = None,
    priority: Annotated[
        int | None,
        typer.Option("--priority", "-p", help="Priority 0-10 (0=lowest, 5=normal, 10=critical)"),
    ] = None,
    expires: Annotated[
        int | None,
        typer.Option("--expires", "-e", help="Days until this memory expires"),
    ] = None,
    project: Annotated[
        str | None,
        typer.Option("--project", "-P", help="Associate with a project (by name)"),
    ] = None,
    shared: Annotated[
        bool, typer.Option("--shared", "-S", help="Use shared/remote storage for this command")
    ] = False,
    force: Annotated[
        bool, typer.Option("--force", "-f", help="Store even if sensitive content detected")
    ] = False,
    redact: Annotated[
        bool, typer.Option("--redact", "-r", help="Auto-redact sensitive content before storing")
    ] = False,
    timestamp: Annotated[
        str | None,
        typer.Option(
            "--timestamp",
            "--at",
            help="ISO datetime of original event (e.g. '2026-03-02T08:00:00'). Defaults to now.",
        ),
    ] = None,
    stdin: Annotated[
        bool,
        typer.Option("--stdin", help="Read content from stdin (safe for shell-special characters)"),
    ] = False,
    json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False,
) -> None:
    """Store a new memory (type auto-detected if not specified).

    Examples:
        pug remember "Fixed auth bug by adding null check"
        pug remember "We decided to use PostgreSQL" --type decision
        pug remember "Need to refactor auth module" --type todo --priority 7
        pug remember "API_KEY=xxx" --redact
        pug remember "Meeting at 8am" --timestamp "2026-03-02T08:00:00"
        echo "content with backticks" | pug remember --stdin --type context
    """
    import sys

    if stdin:
        content = sys.stdin.read().strip()
    if not content:
        typer.secho(
            "Error: content is required (pass as argument or use --stdin).",
            fg=typer.colors.RED,
            err=True,
        )
        raise typer.Exit(1)

    store_content, sensitive_matches = _validate_content(content, force=force, redact=redact)
    mem_type = _resolve_memory_type(memory_type, store_content)
    expiry_days = expires if expires is not None else DEFAULT_EXPIRY_DAYS.get(mem_type)
    mem_priority = Priority.from_int(priority) if priority is not None else Priority.NORMAL

    # Parse --timestamp for original event time
    event_timestamp: datetime | None = None
    if timestamp:
        try:
            event_timestamp = datetime.fromisoformat(timestamp)
            if event_timestamp.tzinfo is not None:
                event_timestamp = event_timestamp.replace(tzinfo=None)
        except (ValueError, TypeError):
            typer.secho(
                f"Invalid timestamp format: {timestamp}. Use ISO format (e.g. '2026-03-02T08:00:00').",
                fg=typer.colors.RED,
                err=True,
            )
            raise typer.Exit(1)

    async def _remember() -> dict[str, Any]:
        config = get_config()
        storage = await get_storage(config, force_shared=shared)

        brain_id: str = (
            storage._current_brain_id or ""
            if hasattr(storage, "_current_brain_id")
            else config.current_brain
        )
        brain = await storage.get_brain(brain_id)
        if not brain:
            return {"error": "No brain configured"}

        project_id = await _resolve_project_id(storage, project)
        if project and project_id is None:
            return {
                "error": f"Project '{project}' not found. Create it with: pug project create \"{project}\""
            }

        enc = await _encode_and_store(
            storage,
            brain.config,
            store_content,
            tags=set(tags) if tags else None,
            mem_type=mem_type,
            mem_priority=mem_priority,
            expiry_days=expiry_days,
            project_id=project_id,
            event_timestamp=event_timestamp,
        )

        response = {
            "message": f"Remembered: {store_content[:50]}{'...' if len(store_content) > 50 else ''}",
            "fiber_id": enc["fiber_id"],
            "memory_type": mem_type.value,
            "priority": mem_priority.name.lower(),
            "neurons_created": enc["neurons_created"],
            "neurons_linked": enc["neurons_linked"],
            "synapses_created": enc["synapses_created"],
        }
        if project_id:
            response["project"] = project
        if enc["typed_mem"].expires_at:
            response["expires_in_days"] = enc["typed_mem"].days_until_expiry
        if force and sensitive_matches:
            response["warnings"] = [
                f"[!] Stored with {len(sensitive_matches)} sensitive item(s) - consider using --redact"
            ]
        return response

    result = run_async(_remember())
    output_result(result, json_output)


def todo(
    task: Annotated[str, typer.Argument(help="Task to remember")],
    priority: Annotated[
        int,
        typer.Option(
            "--priority", "-p", help="Priority 0-10 (default: 5=normal, 7=high, 10=critical)"
        ),
    ] = 5,
    project: Annotated[
        str | None,
        typer.Option("--project", "-P", help="Associate with a project"),
    ] = None,
    expires: Annotated[
        int | None,
        typer.Option("--expires", "-e", help="Days until expiry (default: 30)"),
    ] = None,
    tags: Annotated[
        list[str] | None,
        typer.Option("--tag", "-t", help="Tags for the task"),
    ] = None,
    json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False,
) -> None:
    """Quick shortcut to add a TODO memory.

    Examples:
        pug todo "Fix the login bug"
        pug todo "Review PR #123" --priority 7
    """
    # Determine expiry (default 30 days for todos)
    expiry_days = expires if expires is not None else 30
    mem_priority = Priority.from_int(priority)

    async def _todo() -> dict[str, Any]:
        config = get_config()
        storage = await get_storage(config)

        brain = await storage.get_brain(storage._current_brain_id or "")
        if not brain:
            return {"error": "No brain configured"}

        # Look up project if specified
        project_id = None
        if project:
            proj = await storage.get_project_by_name(project)
            if not proj:
                return {
                    "error": f"Project '{project}' not found. Create it with: pug project create \"{project}\""
                }
            project_id = proj.id

        encoder = MemoryEncoder(storage, brain.config)
        storage.disable_auto_save()

        result = await encoder.encode(
            content=task,
            timestamp=utcnow(),
            tags=set(tags) if tags else None,
        )

        # Create TODO typed memory
        typed_mem = TypedMemory.create(
            fiber_id=result.fiber.id,
            memory_type=MemoryType.TODO,
            priority=mem_priority,
            source="user_input",
            expires_in_days=expiry_days,
            tags=set(tags) if tags else None,
            project_id=project_id,
        )
        await storage.add_typed_memory(typed_mem)
        await storage.batch_save()

        response = {
            "message": f"TODO: {task[:50]}{'...' if len(task) > 50 else ''}",
            "fiber_id": result.fiber.id,
            "memory_type": "todo",
            "priority": mem_priority.name.lower(),
            "expires_in_days": typed_mem.days_until_expiry,
        }

        if project_id:
            response["project"] = project

        return response

    result = run_async(_todo())
    output_result(result, json_output)


async def _gather_freshness(
    storage: PersistentStorage, fiber_ids: list[str]
) -> tuple[list[str], int]:
    """Collect freshness warnings and oldest age from matched fibers."""
    warnings: list[str] = []
    oldest_age = 0
    for fiber_id in fiber_ids:
        fiber = await storage.get_fiber(fiber_id)
        if fiber:
            freshness = evaluate_freshness(fiber.created_at)
            if freshness.warning:
                warnings.append(freshness.warning)
            if freshness.age_days > oldest_age:
                oldest_age = freshness.age_days
    return warnings, oldest_age


def recall(
    query: Annotated[str, typer.Argument(help="Query to search memories")],
    depth: Annotated[
        int | None,
        typer.Option("--depth", "-d", help="Search depth (0=instant, 1=context, 2=habit, 3=deep)"),
    ] = None,
    max_tokens: Annotated[
        int, typer.Option("--max-tokens", "-m", help="Max tokens in response")
    ] = 500,
    min_confidence: Annotated[
        float, typer.Option("--min-confidence", "-c", help="Minimum confidence threshold (0.0-1.0)")
    ] = 0.0,
    shared: Annotated[
        bool, typer.Option("--shared", "-S", help="Use shared/remote storage for this command")
    ] = False,
    show_age: Annotated[
        bool, typer.Option("--show-age", "-a", help="Show memory ages in results")
    ] = True,
    show_routing: Annotated[
        bool, typer.Option("--show-routing", "-R", help="Show query routing info")
    ] = False,
    json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False,
) -> None:
    """Query memories with intelligent routing (query type auto-detected).

    Examples:
        pug recall "What did I do with auth?"
        pug recall "meetings with Alice" --depth 2
        pug recall "Why did the build fail?" --show-routing
        pug recall "project status" --min-confidence 0.5
    """

    async def _recall() -> dict[str, Any]:
        config = get_config()
        storage = await get_storage(config, force_shared=shared)

        brain_id: str = (
            storage._current_brain_id or ""
            if hasattr(storage, "_current_brain_id")
            else config.current_brain
        )
        brain = await storage.get_brain(brain_id)
        if not brain:
            return {"error": "No brain configured"}

        parser = QueryParser()
        router = QueryRouter()
        stimulus = parser.parse(query, reference_time=utcnow())
        route = router.route(stimulus)

        depth_level = (
            DepthLevel(depth) if depth is not None else DepthLevel(min(route.suggested_depth, 3))
        )
        pipeline = ReflexPipeline(storage, brain.config)
        result = await pipeline.query(
            query=query,
            depth=depth_level,
            max_tokens=max_tokens,
            reference_time=utcnow(),
        )

        if result.confidence < min_confidence:
            return {
                "answer": f"No memories found with confidence >= {min_confidence:.2f}",
                "confidence": result.confidence,
                "neurons_activated": result.neurons_activated,
                "below_threshold": True,
            }

        freshness_warnings, oldest_age = await _gather_freshness(
            storage,
            result.fibers_matched or [],
        )

        response = {
            "answer": result.context or "No relevant memories found.",
            "confidence": result.confidence,
            "depth_used": result.depth_used.value,
            "neurons_activated": result.neurons_activated,
            "fibers_matched": result.fibers_matched,
            "latency_ms": result.latency_ms,
        }

        if show_routing:
            response["routing"] = {
                "query_type": route.primary.value,
                "confidence": route.confidence.name.lower(),
                "suggested_depth": route.suggested_depth,
                "use_embeddings": route.use_embeddings,
                "time_weighted": route.time_weighted,
                "signals": list(route.signals)[:5],
            }
        if show_age and oldest_age > 0:
            response["oldest_memory_age"] = format_age(oldest_age)
        if freshness_warnings:
            response["freshness_warnings"] = list(dict.fromkeys(freshness_warnings))[:3]

        return response

    result = run_async(_recall())
    output_result(result, json_output)


def context(
    limit: Annotated[int, typer.Option("--limit", "-l", help="Number of recent memories")] = 10,
    fresh_only: Annotated[
        bool, typer.Option("--fresh-only", help="Only include memories < 30 days old")
    ] = False,
    json_output: Annotated[bool, typer.Option("--json", "-j", help="Output as JSON")] = False,
) -> None:
    """Get recent context (for injecting into AI conversations).

    Examples:
        pug context
        pug context --limit 5 --json
        pug context --fresh-only
    """

    async def _context() -> dict[str, Any]:
        config = get_config()
        storage = await get_storage(config)

        # Get recent fibers
        fibers = await storage.get_fibers(limit=limit * 2 if fresh_only else limit)

        if not fibers:
            return {"context": "No memories stored yet.", "count": 0}

        # Filter by freshness if requested
        now = utcnow()
        if fresh_only:
            fresh_fibers = []
            for fiber in fibers:
                freshness = evaluate_freshness(fiber.created_at, now)
                if freshness.level in (FreshnessLevel.FRESH, FreshnessLevel.RECENT):
                    fresh_fibers.append(fiber)
            fibers = fresh_fibers[:limit]

        # Build context string with age indicators
        context_parts = []
        fiber_data = []

        for fiber in fibers:
            freshness = evaluate_freshness(fiber.created_at, now)
            indicator = get_freshness_indicator(freshness.level)
            age_str = format_age(freshness.age_days)

            fiber_content = fiber.summary
            if not fiber_content and fiber.anchor_neuron_id:
                anchor = await storage.get_neuron(fiber.anchor_neuron_id)
                if anchor:
                    fiber_content = anchor.content

            if fiber_content:
                context_parts.append(f"{indicator} [{age_str}] {fiber_content}")
                fiber_data.append(
                    {
                        "id": fiber.id,
                        "summary": fiber_content,
                        "created_at": fiber.created_at.isoformat(),
                        "age": age_str,
                        "freshness": freshness.level.value,
                    }
                )

        context_str = "\n".join(context_parts) if context_parts else "No context available."

        # Analyze overall freshness
        created_dates = [f.created_at for f in fibers]
        freshness_report = analyze_freshness(created_dates, now)

        return {
            "context": context_str,
            "count": len(fiber_data),
            "fibers": fiber_data,
            "freshness_summary": {
                "fresh": freshness_report.fresh,
                "recent": freshness_report.recent,
                "aging": freshness_report.aging,
                "stale": freshness_report.stale,
                "ancient": freshness_report.ancient,
            },
        }

    result = run_async(_context())
    output_result(result, json_output)


def register(app: typer.Typer) -> None:
    """Register memory commands on the app."""
    app.command()(remember)
    app.command()(todo)
    app.command()(recall)
    app.command()(context)
