"""Live sync of official policy sources used by rule catalogs."""

from __future__ import annotations

import hashlib
import json
import os
import platform
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen


APP_NAME = "oncecheck"
INDEX_FILENAME = "policy_index.json"
STALE_AFTER_DAYS = 14


@dataclass(frozen=True)
class PolicySource:
    id: str
    platform: str
    name: str
    url: str


def _config_dir() -> Path:
    system = platform.system()
    if system == "Darwin":
        base = Path.home() / "Library" / "Application Support"
    elif system == "Windows":
        base = Path(os.environ.get("APPDATA", str(Path.home() / "AppData" / "Roaming")))
    else:
        base = Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
    return base / APP_NAME


def _policy_dir() -> Path:
    return _config_dir() / "policies"


def _index_path() -> Path:
    return _policy_dir() / INDEX_FILENAME


def _source_content_path(source_id: str) -> Path:
    return _policy_dir() / f"{source_id}.html"


def default_policy_sources() -> List[PolicySource]:
    return [
        PolicySource(
            id="ios_app_store_review_guidelines",
            platform="ios",
            name="Apple App Store Review Guidelines",
            url="https://developer.apple.com/app-store/review/guidelines/",
        ),
        PolicySource(
            id="apple_privacy_manifest",
            platform="ios",
            name="Apple Privacy Manifest Files",
            url="https://developer.apple.com/documentation/bundleresources/privacy_manifest_files",
        ),
        PolicySource(
            id="android_play_policy",
            platform="android",
            name="Google Play Developer Policy",
            url="https://play.google.com/about/developer-content-policy/",
        ),
        PolicySource(
            id="android_target_sdk_requirements",
            platform="android",
            name="Google Play Target API Requirements",
            url="https://developer.android.com/google/play/requirements/target-sdk",
        ),
        PolicySource(
            id="owasp_top_10",
            platform="web",
            name="OWASP Top 10",
            url="https://owasp.org/www-project-top-ten/",
        ),
        PolicySource(
            id="w3c_wcag_21",
            platform="web",
            name="WCAG 2.1",
            url="https://www.w3.org/TR/WCAG21/",
        ),
        PolicySource(
            id="w3c_permissions_policy",
            platform="web",
            name="W3C Permissions Policy",
            url="https://www.w3.org/TR/permissions-policy/",
        ),
    ]


def _load_index() -> Dict[str, dict]:
    path = _index_path()
    if not path.exists():
        return {}
    try:
        data = json.loads(path.read_text())
    except (json.JSONDecodeError, OSError):
        return {}
    if isinstance(data, dict):
        return data
    return {}


def _save_index(index: Dict[str, dict]) -> None:
    path = _index_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(".tmp")
    tmp.write_text(json.dumps(index, indent=2, sort_keys=True))
    tmp.replace(path)


def _fetch_url(
    url: str,
    timeout_seconds: int = 20,
    etag: str = "",
    last_modified: str = "",
) -> Tuple[int, bytes, Dict[str, str]]:
    headers = {
        "User-Agent": "oncecheck-cli policy-sync/1.0",
        "Accept": "text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8",
    }
    if etag:
        headers["If-None-Match"] = etag
    if last_modified:
        headers["If-Modified-Since"] = last_modified

    req = Request(url=url, headers=headers, method="GET")
    try:
        with urlopen(req, timeout=timeout_seconds) as resp:
            status = int(getattr(resp, "status", 200))
            body = resp.read() or b""
            resp_headers = {
                "etag": resp.headers.get("ETag", ""),
                "last_modified": resp.headers.get("Last-Modified", ""),
                "content_type": resp.headers.get("Content-Type", ""),
            }
            return status, body, resp_headers
    except HTTPError as exc:
        if exc.code == 304:
            return 304, b"", {}
        raise


def sync_policies(force: bool = False, timeout_seconds: int = 20) -> dict:
    """Fetch official policy pages and store local snapshots/metadata."""
    index = _load_index()
    now = int(time.time())
    results: List[dict] = []
    updated = 0
    unchanged = 0
    failed = 0

    for source in default_policy_sources():
        current = index.get(source.id, {})
        etag = "" if force else str(current.get("etag", ""))
        last_modified = "" if force else str(current.get("last_modified", ""))

        entry = {
            "id": source.id,
            "platform": source.platform,
            "name": source.name,
            "url": source.url,
            "status": "failed",
            "synced_at": current.get("synced_at"),
            "error": "",
        }

        try:
            status_code, body, resp_headers = _fetch_url(
                source.url,
                timeout_seconds=timeout_seconds,
                etag=etag,
                last_modified=last_modified,
            )
            if status_code == 304:
                unchanged += 1
                current["last_checked_at"] = now
                current["last_status"] = 304
                index[source.id] = current
                entry["status"] = "unchanged"
                entry["synced_at"] = current.get("synced_at")
            elif status_code == 200 and body:
                updated += 1
                digest = hashlib.sha256(body).hexdigest()
                content_path = _source_content_path(source.id)
                content_path.parent.mkdir(parents=True, exist_ok=True)
                content_path.write_bytes(body)
                current.update(
                    {
                        "id": source.id,
                        "platform": source.platform,
                        "name": source.name,
                        "url": source.url,
                        "synced_at": now,
                        "last_checked_at": now,
                        "last_status": status_code,
                        "etag": resp_headers.get("etag", ""),
                        "last_modified": resp_headers.get("last_modified", ""),
                        "content_type": resp_headers.get("content_type", ""),
                        "sha256": digest,
                        "content_path": str(content_path),
                    }
                )
                index[source.id] = current
                entry["status"] = "updated"
                entry["synced_at"] = now
            else:
                failed += 1
                current["last_checked_at"] = now
                current["last_status"] = status_code
                index[source.id] = current
                entry["status"] = "failed"
                entry["error"] = f"unexpected status {status_code}"
        except (URLError, HTTPError, TimeoutError, OSError) as exc:
            failed += 1
            current["last_checked_at"] = now
            current["last_status"] = 0
            current["last_error"] = str(exc)
            index[source.id] = current
            entry["status"] = "failed"
            entry["error"] = str(exc)

        results.append(entry)

    _save_index(index)
    return {
        "updated": updated,
        "unchanged": unchanged,
        "failed": failed,
        "results": results,
        "total": len(results),
    }


def get_policy_sync_status(stale_after_days: int = STALE_AFTER_DAYS) -> dict:
    """Return status summary including freshness of synced policies."""
    index = _load_index()
    now = int(time.time())
    stale_after_seconds = stale_after_days * 24 * 3600
    stale = 0
    missing = 0

    statuses: List[dict] = []
    for source in default_policy_sources():
        entry = index.get(source.id, {})
        synced_at = int(entry.get("synced_at", 0) or 0)
        if not synced_at:
            missing += 1
        age_seconds = max(0, now - synced_at) if synced_at else None
        is_stale = synced_at > 0 and age_seconds is not None and age_seconds > stale_after_seconds
        if is_stale:
            stale += 1
        statuses.append(
            {
                "id": source.id,
                "platform": source.platform,
                "name": source.name,
                "url": source.url,
                "synced_at": synced_at or None,
                "last_status": entry.get("last_status"),
                "stale": is_stale,
                "missing": synced_at == 0,
            }
        )

    return {
        "total": len(statuses),
        "stale": stale,
        "missing": missing,
        "healthy": stale == 0 and missing == 0,
        "sources": statuses,
    }


def maybe_auto_sync(stale_after_days: int = STALE_AFTER_DAYS) -> Optional[dict]:
    """Sync only if missing/stale data is detected. Returns sync result or None."""
    status = get_policy_sync_status(stale_after_days=stale_after_days)
    if status["healthy"]:
        return None
    return sync_policies(force=False)

