"""Handle push events — detect spec file changes and run forward sync."""

from __future__ import annotations

import logging

from specwright import analytics

from ...parser.models import ParseOptions
from ...parser.parse import parse_spec
from ...sync.adapters.base import TicketAdapter
from ...sync.adapters.factory import create_adapter, from_config
from ...sync.engine import forward_sync
from ...sync.mapping import (
    TicketMappingConfig,
    TicketSystemConfig,
    deep_merge_configs,
    synthesize_mapping_config,
)
from ...sync.org_config import load_org_mapping_config
from ...sync.router import resolve_target
from ..spec_utils import filter_spec_files, load_repo_config, matches_doc_patterns

logger = logging.getLogger(__name__)

BOT_SUFFIX = "[bot]"


def _invalidate_web_cache(owner: str, repo: str) -> None:
    """Invalidate cached web data for a repo after a push."""
    try:
        from specwright.main import app

        cache = getattr(app.state, "cache", None)
        if cache is not None:
            cache.invalidate(f"repo:{owner}/{repo}")
            cache.invalidate(f"config:{owner}/{repo}")
            cache.invalidate_prefix(f"spec:{owner}/{repo}/")
            cache.invalidate_prefix(f"doc:{owner}/{repo}/")
            cache.invalidate_prefix("org_overview:")
            cache.invalidate_prefix("search:")
            cache.invalidate_prefix("facets:")
            logger.info("Invalidated web cache for %s/%s", owner, repo)
    except Exception:
        pass  # Cache not available (e.g. during tests)


async def _index_specs(
    owner: str,
    repo: str,
    parsed_specs: dict,
    removed_spec_files: set[str],
    commit_sha: str,
) -> None:
    """Best-effort indexing of changed/removed specs into the search index."""
    try:
        from specwright.main import app
        from specwright.search.indexer import index_spec

        search_index = getattr(app.state, "search_index", None)
        if search_index is None:
            return

        embed_client = getattr(app.state, "embed_client", None)
        full_repo = f"{owner}/{repo}"

        for file_path in removed_spec_files:
            try:
                await search_index.delete_spec(full_repo, file_path)
                logger.info("Deleted spec from index: %s:%s", full_repo, file_path)
            except Exception:
                logger.warning(
                    "Failed to delete spec from index: %s:%s", full_repo, file_path, exc_info=True
                )

        for file_path, doc in parsed_specs.items():
            try:
                await index_spec(
                    doc=doc,
                    repo=full_repo,
                    search_index=search_index,
                    embed_client=embed_client,
                    commit_sha=commit_sha,
                )
            except Exception:
                logger.warning("Failed to index spec: %s:%s", full_repo, file_path, exc_info=True)
    except Exception:
        pass  # Search infrastructure not available (e.g. during tests)


def _get_doc_patterns(owner: str, repo: str) -> list[str] | None:
    """Load doc_paths from SPECWRIGHT.yaml config cache, or None for defaults."""
    try:
        from specwright.main import app

        cache = getattr(app.state, "cache", None)
        if cache is None:
            return None
        cached = cache.get(f"config:{owner}/{repo}")
        if cached is not None:
            return cached.specs.doc_paths
    except Exception:
        pass
    return None


async def _index_doc_files(
    client,
    owner: str,
    repo: str,
    changed_files: set[str],
    removed_files: set[str],
    doc_patterns: list[str],
    commit_sha: str,
) -> None:
    """Best-effort indexing of changed doc files (not just spec files)."""
    try:
        from specwright.main import app
        from specwright.search.indexer import index_spec

        search_index = getattr(app.state, "search_index", None)
        if search_index is None:
            return

        embed_client = getattr(app.state, "embed_client", None)
        full_repo = f"{owner}/{repo}"

        # Delete removed doc files from index
        for file_path in removed_files:
            if matches_doc_patterns(file_path, doc_patterns):
                try:
                    await search_index.delete_spec(full_repo, file_path)
                    logger.info("Deleted doc from index: %s:%s", full_repo, file_path)
                except Exception:
                    logger.warning(
                        "Failed to delete doc: %s:%s", full_repo, file_path, exc_info=True
                    )

        # Index changed doc files that aren't already spec files
        for file_path in changed_files:
            if not matches_doc_patterns(file_path, doc_patterns):
                continue
            # Skip if already handled as a spec file
            if filter_spec_files([file_path]):
                continue
            try:
                content, _sha = await client.get_file_content(owner, repo, file_path)
                result = parse_spec(content, ParseOptions(file_path=file_path))
                await index_spec(
                    doc=result.document,
                    repo=full_repo,
                    search_index=search_index,
                    embed_client=embed_client,
                    commit_sha=commit_sha,
                )
            except Exception:
                logger.warning("Failed to index doc: %s:%s", full_repo, file_path, exc_info=True)
    except Exception:
        pass


async def _track_code_changes(owner: str, repo: str, changed_paths: list[str]) -> None:
    """Best-effort: update last_code_change_at for docs related to changed code."""
    try:
        from specwright.main import app

        search_index = getattr(app.state, "search_index", None)
        if search_index is None:
            return

        full_repo = f"{owner}/{repo}"
        newly_stale = await search_index.mark_code_change(full_repo, changed_paths)
        if newly_stale:
            logger.info(
                "Marked %d doc(s) as stale in %s",
                len(newly_stale),
                full_repo,
            )
    except Exception:
        pass  # Best-effort — don't break push handling


def _resolve_adapter(
    mapping: TicketMappingConfig,
    doc,
    project_key: str,
    file_path: str,
) -> tuple[TicketAdapter | None, str, TicketSystemConfig | None]:
    """Resolve a ticket adapter for a spec doc using routing or legacy fallback.

    Returns (adapter, project_key, system_config) or (None, "", None) when
    no adapter can be resolved.
    """
    adapter = None
    sys_config: TicketSystemConfig | None = None

    if not mapping.is_empty():
        # Try multi-system routing first, then single-system fallback
        target_name = resolve_target(None, doc, mapping.routing, mapping.ticket_systems)
        if target_name:
            sys_config = mapping.ticket_systems[target_name]
            # Empty dict → None so from_config uses default env var detection
            adapter = from_config(target_name, sys_config, mapping.auth_profiles or None)
            project_key = project_key or sys_config.project or ""
        else:
            single = mapping.single_system()
            if single:
                sys_config = single
                sys_name = next(iter(mapping.ticket_systems.keys()))
                adapter = from_config(sys_name, single, mapping.auth_profiles or None)
                project_key = project_key or single.project or ""

    if not adapter:
        if not project_key:
            logger.info("No ticket_project in frontmatter for %s, skipping sync", file_path)
            return None, "", None
        adapter = create_adapter(ticket_project=project_key)

    if not adapter:
        logger.info("No ticket adapter configured for %s, skipping sync", file_path)
        return None, "", None

    if not project_key:
        logger.info("No project key resolved for %s, skipping sync", file_path)
        return None, "", None

    return adapter, project_key, sys_config


async def on_push(client, payload: dict) -> None:
    """Handle a GitHub push event.

    Args:
        client: GitHubClient instance.
        payload: The webhook payload.
    """
    commits = payload.get("commits", [])

    # Loop prevention: skip if latest commit is from the bot
    if commits:
        latest = commits[-1]
        author_name = (latest.get("author") or {}).get("name", "")
        if author_name.endswith(BOT_SUFFIX):
            logger.info("Skipping bot-authored push")
            return

    # Collect changed and removed files separately
    changed_files: set[str] = set()
    removed_files: set[str] = set()
    for commit in commits:
        for f in commit.get("added", []):
            changed_files.add(f)
        for f in commit.get("modified", []):
            changed_files.add(f)
        for f in commit.get("removed", []):
            removed_files.add(f)

    # Files that were removed shouldn't be in changed
    changed_files -= removed_files

    all_touched = changed_files | removed_files
    spec_files = filter_spec_files(list(all_touched))

    ref_raw = payload.get("ref", "")
    ref = ref_raw.replace("refs/heads/", "")
    owner = payload["repository"]["owner"]["login"]
    repo = payload["repository"]["name"]

    # Invalidate web cache on any push (spec or doc changes)
    _invalidate_web_cache(owner, repo)

    if not spec_files:
        return

    logger.info("Spec files changed in push: %s ref=%s", spec_files, ref)

    # Track removed spec files and parsed docs for indexing
    removed_spec_files = filter_spec_files(list(removed_files))
    parsed_specs: dict = {}
    commit_sha = payload.get("after", "")

    # Load repo config for require_review setting
    config = await load_repo_config(client, owner, repo, ref=ref)
    require_review = config.specs.require_review

    # Resolve ticket mapping config (new-style or legacy)
    mapping, _is_deprecated = synthesize_mapping_config(
        ticket_system=config.ticket_system,
        project_key=config.project_key,
        ticket_mapping=config.ticket_mapping,
    )

    # Merge org-level defaults if available
    org_mapping = await load_org_mapping_config(client, owner)
    if org_mapping:
        mapping = deep_merge_configs(org_mapping, mapping)

    for file_path in spec_files:
        if file_path in removed_files:
            continue  # Skip removed files for sync (handled by indexing)

        try:
            content, file_sha = await client.get_file_content(owner, repo, file_path, ref=ref)
            result = parse_spec(content, ParseOptions(file_path=file_path))
            parsed_specs[file_path] = result.document

            project_key = result.document.frontmatter.ticket_project

            adapter, project_key, sys_config = _resolve_adapter(
                mapping, result.document, project_key, file_path
            )
            if not adapter or not project_key:
                continue

            markdown, sync_result = await forward_sync(
                result.document,
                adapter,
                project_key,
                require_review=require_review,
                system_config=sys_config,
            )

            logger.info(
                "Forward sync complete for %s: created=%d errors=%d",
                file_path,
                len(sync_result.created),
                len(sync_result.errors),
            )

            # Commit updated markdown if tickets were created
            if sync_result.created:
                await client.create_or_update_file(
                    owner,
                    repo,
                    file_path,
                    markdown,
                    f"chore(specwright): add ticket links to {file_path}",
                    file_sha,
                    branch=ref,
                )
        except Exception:
            logger.exception("Error during forward sync for %s", file_path)

    # Best-effort: index changed/removed specs
    await _index_specs(
        owner,
        repo,
        parsed_specs,
        set(removed_spec_files),
        commit_sha,
    )

    # Best-effort: index doc files matching configurable doc_paths
    doc_patterns = _get_doc_patterns(owner, repo)
    if doc_patterns:
        await _index_doc_files(
            client,
            owner,
            repo,
            changed_files,
            removed_files,
            doc_patterns,
            commit_sha,
        )

    # Lightweight staleness tracking: mark code changes for non-spec files
    non_spec_changed = changed_files - set(spec_files)
    if non_spec_changed:
        await _track_code_changes(owner, repo, list(non_spec_changed))

    if parsed_specs:
        analytics.track(
            "forward_sync_completed",
            properties={
                "repo": f"{owner}/{repo}",
                "spec_files_synced": len(parsed_specs),
            },
            groups={"organization": owner},
        )
