"""Local data cache for incremental syncs.

Stores raw source data (Jira issues, Slack messages, web pages) as JSON
files on disk, separate from the vector DB. Tracks last-sync timestamps
per source to enable incremental fetching — only new/modified data is
downloaded on subsequent runs.

Cache structure:
    ~/.local/share/redhat-oracle/cache/
    ├── jira/
    │   ├── RHCLOUD.json        # All issues for project
    │   └── sync_state.json     # Last sync timestamp per project
    ├── slack/
    │   ├── channels.json       # All messages by channel
    │   └── sync_state.json
    └── web/
        ├── pages.json          # Cached page content
        └── sync_state.json
"""

from __future__ import annotations

import json
import logging
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, cast

from redhat_oracle.core.config import OracleConfig, get_data_dir

logger = logging.getLogger(__name__)


class SyncCache:
    """Local cache with per-source sync state tracking."""

    def __init__(self, config: OracleConfig) -> None:
        self._base = get_data_dir(config) / "cache"
        self._base.mkdir(parents=True, exist_ok=True)

    def _source_dir(self, source: str) -> Path:
        d = self._base / source
        d.mkdir(parents=True, exist_ok=True)
        return d

    def _state_path(self, source: str) -> Path:
        return self._source_dir(source) / "sync_state.json"

    def _load_state(self, source: str) -> dict[str, Any]:
        path = self._state_path(source)
        if path.exists():
            try:
                return cast(dict[str, Any], json.loads(path.read_text()))
            except (json.JSONDecodeError, OSError):
                pass
        return {}

    def _save_state(self, source: str, state: dict[str, Any]) -> None:
        path = self._state_path(source)
        path.write_text(json.dumps(state, indent=2))

    # --- Jira ---

    def get_jira_last_sync(self, project: str) -> str | None:
        """Get the last sync timestamp for a Jira project (ISO 8601)."""
        state = self._load_state("jira")
        return state.get(f"last_sync_{project}")

    def set_jira_last_sync(self, project: str, timestamp: str) -> None:
        """Set the last sync timestamp for a Jira project."""
        state = self._load_state("jira")
        state[f"last_sync_{project}"] = timestamp
        self._save_state("jira", state)

    def load_jira_issues(self, project: str) -> list[dict[str, Any]]:
        """Load cached Jira issues for a project."""
        path = self._source_dir("jira") / f"{project}.json"
        if path.exists():
            try:
                return cast(list[dict[str, Any]], json.loads(path.read_text()))
            except (json.JSONDecodeError, OSError):
                pass
        return []

    def save_jira_issues(self, project: str, issues: list[dict[str, Any]]) -> None:
        """Save Jira issues to cache, merging with existing."""
        path = self._source_dir("jira") / f"{project}.json"
        path.write_text(json.dumps(issues, indent=2))

    def merge_jira_issues(
        self,
        project: str,
        new_issues: list[dict[str, Any]],
    ) -> list[dict[str, Any]]:
        """Merge new/updated issues into the cache. Returns all issues."""
        existing = self.load_jira_issues(project)

        # Index existing by key for fast lookup
        by_key: dict[str, dict[str, Any]] = {i["key"]: i for i in existing}

        # Merge — new/updated issues overwrite existing
        added = 0
        updated = 0
        for issue in new_issues:
            key = issue["key"]
            if key in by_key:
                updated += 1
            else:
                added += 1
            by_key[key] = issue

        all_issues = list(by_key.values())
        self.save_jira_issues(project, all_issues)

        if added or updated:
            logger.info(
                "Jira %s cache: %d added, %d updated, %d total",
                project, added, updated, len(all_issues),
            )

        return all_issues

    # --- Slack ---

    def get_slack_last_sync(self) -> str | None:
        """Get the last Slack sync timestamp."""
        state = self._load_state("slack")
        return state.get("last_sync")

    def set_slack_last_sync(self, timestamp: str) -> None:
        state = self._load_state("slack")
        state["last_sync"] = timestamp
        self._save_state("slack", state)

    def load_slack_messages(self) -> list[dict[str, Any]]:
        path = self._source_dir("slack") / "messages.json"
        if path.exists():
            try:
                return cast(list[dict[str, Any]], json.loads(path.read_text()))
            except (json.JSONDecodeError, OSError):
                pass
        return []

    def save_slack_messages(self, messages: list[dict[str, Any]]) -> None:
        path = self._source_dir("slack") / "messages.json"
        path.write_text(json.dumps(messages, indent=2))

    # --- Web ---

    def get_web_last_sync(self) -> str | None:
        state = self._load_state("web")
        return state.get("last_sync")

    def set_web_last_sync(self, timestamp: str) -> None:
        state = self._load_state("web")
        state["last_sync"] = timestamp
        self._save_state("web", state)

    def load_web_pages(self) -> list[dict[str, Any]]:
        path = self._source_dir("web") / "pages.json"
        if path.exists():
            try:
                return cast(list[dict[str, Any]], json.loads(path.read_text()))
            except (json.JSONDecodeError, OSError):
                pass
        return []

    def save_web_pages(self, pages: list[dict[str, Any]]) -> None:
        path = self._source_dir("web") / "pages.json"
        path.write_text(json.dumps(pages, indent=2))

    # --- Slack per-day tracking ---

    def get_slack_completed_days(self) -> set[str]:
        """Get the set of completed YYYY-MM-DD strings for Slack sync."""
        state = self._load_state("slack")
        return set(state.get("completed_days", []))

    def add_slack_completed_day(self, date_str: str) -> None:
        """Mark a day as completed for Slack sync."""
        state = self._load_state("slack")
        days = set(state.get("completed_days", []))
        days.add(date_str)
        state["completed_days"] = sorted(days)
        self._save_state("slack", state)

    def clear_slack_completed_days(self) -> None:
        """Clear all completed day tracking for Slack."""
        state = self._load_state("slack")
        state.pop("completed_days", None)
        self._save_state("slack", state)

    # --- GitHub comments per-repo tracking ---

    def is_github_repo_comments_done(self, org: str, repo: str) -> bool:
        """Check if comments for a GitHub repo have been fully indexed."""
        state = self._load_state("github_comments")
        return f"{org}/{repo}" in state.get("completed_repos", {})

    def mark_github_repo_comments_done(
        self, org: str, repo: str, count: int
    ) -> None:
        """Mark a GitHub repo's comments as fully indexed."""
        state = self._load_state("github_comments")
        completed = state.setdefault("completed_repos", {})
        completed[f"{org}/{repo}"] = {
            "chunks": count,
            "completed_at": datetime.now(UTC).isoformat(),
        }
        self._save_state("github_comments", state)

    def clear_github_comments_state(self) -> None:
        """Clear all GitHub comments tracking state."""
        state = self._load_state("github_comments")
        state.pop("completed_repos", None)
        self._save_state("github_comments", state)

    # --- GitLab comments per-project tracking ---

    def is_gitlab_project_comments_done(
        self, host: str, namespace: str
    ) -> bool:
        """Check if comments for a GitLab project have been fully indexed."""
        state = self._load_state("gitlab_comments")
        return f"{host}/{namespace}" in state.get("completed_projects", {})

    def mark_gitlab_project_comments_done(
        self, host: str, namespace: str, count: int
    ) -> None:
        """Mark a GitLab project's comments as fully indexed."""
        state = self._load_state("gitlab_comments")
        completed = state.setdefault("completed_projects", {})
        completed[f"{host}/{namespace}"] = {
            "chunks": count,
            "completed_at": datetime.now(UTC).isoformat(),
        }
        self._save_state("gitlab_comments", state)

    def clear_gitlab_comments_state(self) -> None:
        """Clear all GitLab comments tracking state."""
        state = self._load_state("gitlab_comments")
        state.pop("completed_projects", None)
        self._save_state("gitlab_comments", state)

    # --- Web per-site tracking ---

    def is_web_site_done(self, url: str) -> bool:
        """Check if a web site has been fully crawled and indexed."""
        state = self._load_state("web")
        return url in state.get("completed_sites", {})

    def mark_web_site_done(self, url: str, pages: int, chunks: int) -> None:
        """Mark a web site as fully crawled and indexed."""
        state = self._load_state("web")
        completed = state.setdefault("completed_sites", {})
        completed[url] = {
            "pages": pages,
            "chunks": chunks,
            "completed_at": datetime.now(UTC).isoformat(),
        }
        self._save_state("web", state)

    def clear_web_state(self) -> None:
        """Clear all web site tracking state."""
        state = self._load_state("web")
        state.pop("completed_sites", None)
        self._save_state("web", state)

    def cache_stats(self) -> dict[str, Any]:
        """Return cache sizes per source."""
        stats: dict[str, Any] = {}
        for source in ("jira", "slack", "web"):
            source_dir = self._base / source
            if source_dir.exists():
                files = list(source_dir.glob("*.json"))
                total_size = sum(f.stat().st_size for f in files)
                stats[source] = {
                    "files": len(files),
                    "size_mb": round(total_size / (1024 * 1024), 1),
                }
            else:
                stats[source] = {"files": 0, "size_mb": 0}
        return stats
