from __future__ import annotations

import asyncio
import json
import re
import subprocess
import time
from dataclasses import dataclass, field
from pathlib import Path

from .config import (
    ConfigurationError,
    get_kb_root,
    get_project_kb_root,
    get_user_kb_root,
)
from .context import KBContext, get_kb_context, matches_glob
from .parser import ParseError, parse_entry

CACHE_DIR = Path("/tmp")
CACHE_TTL_SECONDS = 3600  # 1 hour
DEFAULT_MAX_ENTRIES = 4
CACHE_VERSION = 2


@dataclass
class SessionContextResult:
    project: str
    entries: list[dict]
    content: str
    recent_entries: list[dict] = field(default_factory=list)
    cached: bool = False


def _get_cache_path(project_name: str, max_entries: int) -> Path:
    safe_name = re.sub(r"[^\w\-]", "_", project_name)
    return CACHE_DIR / f"memex-context-{safe_name}-{max_entries}-v{CACHE_VERSION}.json"


def _load_cached_context(project_name: str, max_entries: int) -> SessionContextResult | None:
    cache_path = _get_cache_path(project_name, max_entries)
    if not cache_path.exists():
        return None

    try:
        data = json.loads(cache_path.read_text())
        if time.time() - data.get("timestamp", 0) < CACHE_TTL_SECONDS:
            return SessionContextResult(
                project=data.get("project", project_name),
                entries=data.get("entries", []),
                recent_entries=data.get("recent_entries", []),
                content=data.get("content", ""),
                cached=True,
            )
    except (json.JSONDecodeError, OSError):
        return None

    return None


def _save_cached_context(result: SessionContextResult, max_entries: int) -> None:
    cache_path = _get_cache_path(result.project, max_entries)
    payload = {
        "timestamp": time.time(),
        "project": result.project,
        "entries": result.entries,
        "recent_entries": result.recent_entries,
        "content": result.content,
    }
    try:
        cache_path.write_text(json.dumps(payload))
    except OSError:
        pass


def _get_git_remote(cwd: Path) -> str | None:
    try:
        result = subprocess.run(
            ["git", "remote", "get-url", "origin"],
            capture_output=True,
            text=True,
            timeout=5,
            cwd=cwd,
        )
        if result.returncode == 0:
            return result.stdout.strip()
    except (subprocess.TimeoutExpired, OSError):
        return None
    return None


def _extract_project_name(remote_url: str | None, cwd: Path) -> str:
    if remote_url:
        ssh_match = re.search(r":([^/]+/[^/]+?)(?:\.git)?$", remote_url)
        if ssh_match:
            return ssh_match.group(1).split("/")[-1]

        https_match = re.search(r"/([^/]+?)(?:\.git)?$", remote_url)
        if https_match:
            return https_match.group(1)

    return cwd.name


def _resolve_project_name(cwd: Path, context: KBContext | None) -> str:
    if context:
        project_name = context.get_project_name()
        if project_name:
            return project_name

    remote_url = _get_git_remote(cwd)
    return _extract_project_name(remote_url, cwd)


def _get_project_tokens(project_name: str, cwd: Path) -> set[str]:
    tokens = set()

    for part in re.split(r"[-_]", project_name.lower()):
        if len(part) > 2:
            tokens.add(part)

    if (cwd / "pyproject.toml").exists() or (cwd / "requirements.txt").exists():
        tokens.update({"python", "uv", "pip"})

    if (cwd / "Dockerfile").exists():
        tokens.update({"docker", "deployment"})

    if (cwd / "package.json").exists():
        tokens.update({"node", "npm", "javascript", "typescript"})

    if (cwd / "Cargo.toml").exists():
        tokens.update({"rust", "cargo"})

    if (cwd / ".devcontainer").exists():
        tokens.add("devcontainer")

    return tokens


def _extract_summary(content: str, max_length: int = 150) -> str:
    lines: list[str] = []
    for line in content.split("\n"):
        stripped = line.strip()
        if stripped and not stripped.startswith("#"):
            lines.append(stripped)
            if len(" ".join(lines)) > max_length:
                break

    summary = " ".join(lines)
    if len(summary) > max_length:
        summary = summary[:max_length].rsplit(" ", 1)[0] + "..."
    return summary


def _scan_kb_entries(kb_root: Path) -> list[dict]:
    entries: list[dict] = []

    for md_file in kb_root.rglob("*.md"):
        if md_file.name.startswith("_"):
            continue

        try:
            metadata, content, _ = parse_entry(md_file)
            relative_path = md_file.relative_to(kb_root)
            tags_str = " ".join(metadata.tags) if metadata.tags else ""

            entries.append(
                {
                    "path": str(relative_path),
                    "title": metadata.title,
                    "tags": tags_str,
                    "summary": _extract_summary(content),
                    "category": relative_path.parent.name if relative_path.parent.name else "root",
                }
            )
        except (OSError, UnicodeDecodeError, ParseError):
            continue

    return entries


def _score_entry(
    entry: dict, project_name: str, project_tokens: set[str], context: KBContext | None
) -> float:
    score = 0.0
    entry_text = f"{entry['title']} {entry['tags']} {entry['category']}".lower()

    if project_name.lower() in entry_text:
        score += 10.0

    for token in project_tokens:
        if token in entry_text:
            score += 2.0

    if context:
        boost_paths = context.get_all_boost_paths()
        if boost_paths:
            for pattern in boost_paths:
                if matches_glob(entry["path"], pattern):
                    score += 4.0
                    break
        if context.primary and matches_glob(entry["path"], context.primary):
            score += 2.0

    category_weights = {
        "devops": 1.5,
        "development": 1.5,
        "patterns": 1.2,
        "architecture": 1.0,
        "infrastructure": 1.0,
    }
    score *= category_weights.get(entry["category"], 1.0)

    return score


def _select_relevant_entries(
    entries: list[dict],
    project_name: str,
    project_tokens: set[str],
    context: KBContext | None,
    max_entries: int,
) -> list[dict]:
    if not entries:
        return []

    scored = [
        (entry, _score_entry(entry, project_name, project_tokens, context)) for entry in entries
    ]
    scored.sort(key=lambda x: x[1], reverse=True)

    return [entry for entry, score in scored[:max_entries] if score > 0]


def _resolve_recent_scope() -> str | None:
    project_kb = get_project_kb_root()
    if project_kb and project_kb.exists():
        return "project"

    user_kb = get_user_kb_root()
    if user_kb and user_kb.exists():
        return "user"

    return None


def _get_recent_entries(scope: str | None, limit: int = 5) -> list[dict]:
    from .core import whats_new as core_whats_new

    try:
        return asyncio.run(core_whats_new(days=14, limit=limit, scope=scope))
    except Exception:
        return []


def _format_output(
    entries: list[dict],
    project_name: str,
    *,
    kb_root: Path,
    scope: str | None,
    context: KBContext | None,
    recent_entries: list[dict],
) -> str:
    lines = [
        "## Memex Knowledge Base",
        "",
        "You have access to a knowledge base with documentation, patterns,",
        "and operational guides. **Search before creating** to avoid duplicates.",
        "",
        "**Project Context:**",
    ]
    context_parts = []
    if scope:
        context_parts.append(f"scope={scope}")
    context_parts.append(f"kb_path={kb_root}")
    if context and context.primary:
        context_parts.append(f"primary={context.primary}")
    if context and context.default_tags:
        context_parts.append(f"default_tags={','.join(context.default_tags)}")
    if context and context.paths:
        context_parts.append(f"boost_paths={','.join(context.paths)}")
    lines.append(" | ".join(context_parts))
    lines.append("")
    lines.extend(
        [
            "**Quick Reference:**",
            "| Action | Command |",
            "|--------|---------|",
            "| Search | `mx search <query>` |",
            "| Browse | `mx list` or `mx tree` |",
            "| Read entry | `mx get <path>` |",
            "| Add new | `mx add --title=... --tags=... --content=...` |",
            "",
        ]
    )

    if entries:
        lines.append(f"**Relevant entries for {project_name}:**")
        lines.append("")
        for entry in entries:
            tags = f" [{entry['tags']}]" if entry["tags"] else ""
            lines.append(f"- **{entry['title']}** (`{entry['path']}`){tags}")
        lines.append("")

    if recent_entries:
        lines.append("**Recent Entries:**")
        lines.append("")
        for entry in recent_entries:
            activity = "NEW" if entry.get("activity_type") == "created" else "UPD"
            date_str = str(entry.get("activity_date", ""))
            path = entry.get("path", "")
            title = entry.get("title", "Untitled")
            lines.append(f"- {date_str} {activity} `{path}` — {title}")
        lines.append("")

    lines.append("**Next Commands:**")
    lines.append("")
    if scope:
        lines.append(f"- `mx whats-new --scope={scope} --days=7`")
    else:
        lines.append("- `mx whats-new --days=7`")
    lines.append("- `mx tags`")
    lines.append("")

    return "\n".join(lines)


def build_session_context(*, max_entries: int = DEFAULT_MAX_ENTRIES) -> SessionContextResult | None:
    cwd = Path.cwd()
    context = get_kb_context()
    project_name = _resolve_project_name(cwd, context)

    cached = _load_cached_context(project_name, max_entries)
    if cached:
        return cached

    try:
        kb_root = get_kb_root()
    except ConfigurationError:
        return None

    if not kb_root.exists():
        return None

    project_tokens = _get_project_tokens(project_name, cwd)
    entries = _scan_kb_entries(kb_root)
    relevant = _select_relevant_entries(entries, project_name, project_tokens, context, max_entries)
    scope = _resolve_recent_scope()
    recent_entries = _get_recent_entries(scope)
    content = _format_output(
        relevant,
        project_name,
        kb_root=kb_root,
        scope=scope,
        context=context,
        recent_entries=recent_entries,
    )

    result = SessionContextResult(
        project=project_name,
        entries=relevant,
        recent_entries=recent_entries,
        content=content,
        cached=False,
    )
    _save_cached_context(result, max_entries)
    return result


def find_git_root(start_dir: Path) -> Path | None:
    try:
        result = subprocess.run(
            ["git", "rev-parse", "--show-toplevel"],
            capture_output=True,
            text=True,
            check=True,
            cwd=start_dir,
        )
        return Path(result.stdout.strip())
    except subprocess.CalledProcessError:
        return None


def default_settings_path(start_dir: Path) -> Path:
    repo_root = find_git_root(start_dir) or start_dir
    return repo_root / ".claude" / "settings.json"


def _load_settings(settings_path: Path) -> dict:
    if not settings_path.exists():
        return {}

    data = json.loads(settings_path.read_text())
    if not isinstance(data, dict):
        raise ValueError("Claude settings must be a JSON object")
    return data


def _remove_setup_remote(hook: dict) -> bool:
    command = hook.get("command")
    return isinstance(command, str) and "setup-remote.sh" in command


def _ensure_session_start(settings: dict, command: str) -> None:
    hooks = settings.setdefault("hooks", {})
    if not isinstance(hooks, dict):
        raise ValueError("Claude settings 'hooks' must be a JSON object")

    session_start = hooks.setdefault("SessionStart", [])
    if not isinstance(session_start, list):
        raise ValueError("Claude settings 'hooks.SessionStart' must be a list")

    use_nested = any(isinstance(entry, dict) and "hooks" in entry for entry in session_start)

    if use_nested:
        if not session_start:
            session_start.append({"hooks": []})

        for entry in session_start:
            if not isinstance(entry, dict):
                continue
            hooks_list = entry.get("hooks")
            if not isinstance(hooks_list, list):
                hooks_list = []
                entry["hooks"] = hooks_list

            hooks_list[:] = [
                hook
                for hook in hooks_list
                if not (isinstance(hook, dict) and _remove_setup_remote(hook))
            ]

            if not any(
                isinstance(hook, dict)
                and hook.get("type") == "command"
                and hook.get("command") == command
                for hook in hooks_list
            ):
                hooks_list.append({"type": "command", "command": command})
    else:
        session_start[:] = [
            entry
            for entry in session_start
            if not (isinstance(entry, dict) and _remove_setup_remote(entry))
        ]

        if not any(
            isinstance(entry, dict) and entry.get("command") == command for entry in session_start
        ):
            session_start.append({"command": command})


def install_session_hook(settings_path: Path, *, command: str = "mx session-context") -> Path:
    settings_path = settings_path.expanduser()
    settings_path.parent.mkdir(parents=True, exist_ok=True)

    settings = _load_settings(settings_path)
    _ensure_session_start(settings, command)
    settings_path.write_text(json.dumps(settings, indent=2) + "\n")
    return settings_path
