#!/usr/bin/env python3
"""
brain.py — GSC Daily (greatcareer.co.com)
==========================================
  1. Fetch robots.txt → parse all sitemaps
  2. Submit all sitemaps + feed.xml + robots.txt
  3. Inspect home URL

Rotates through multiple service accounts on quota errors.

Requirements:
    pip install google-api-python-client google-auth google-auth-httplib2 requests
"""

import logging
import sys
import time
import requests
from datetime import datetime
from pathlib import Path

from google.oauth2 import service_account
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError


# ╔═════════════════════════════════════════════════════════════╗
# ║  HARDCODED CONFIG                                          ║
# ╚═════════════════════════════════════════════════════════════╝
SITE_URL = "https://greatcareer.co.com"

# Add as many service account JSON files as you want.
# When one hits quota, it auto-switches to the next.
# All these service account emails must be added as Owner/Full user in GSC.
GSC_CREDS_LIST = [
    "gsc-bot.json",
    "gsc-bot-2.json",
    "gsc-bot-3.json",
    # "gsc-bot-4.json",
    # "gsc-bot-5.json",
    # ... add more as needed
]
# ─────────────────────────────────────────────────────────────

SCOPES = [
    "https://www.googleapis.com/auth/webmasters",
    "https://www.googleapis.com/auth/indexing",
]

LOG_DIR = "gsc_logs"
MAX_RETRIES = 5
BASE_DELAY = 2
DELAY_BETWEEN_SITEMAPS = 0.8


# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
def setup_logging() -> Path:
    Path(LOG_DIR).mkdir(exist_ok=True)
    site_slug = SITE_URL.replace("https://", "").replace("/", "_").rstrip("_")
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    log_file = Path(LOG_DIR) / f"{site_slug}_{timestamp}.log"

    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s  %(message)s",
        datefmt="%H:%M:%S",
        handlers=[
            logging.FileHandler(log_file, encoding="utf-8"),
            logging.StreamHandler(sys.stdout),
        ],
    )
    return log_file


# ---------------------------------------------------------------------------
# Service Account Rotator
# ---------------------------------------------------------------------------
class ServiceRotator:
    """Rotates through multiple service accounts. Skips exhausted ones."""

    def __init__(self, creds_files: list[str]):
        # Filter to only files that exist
        self.creds_files = [f for f in creds_files if Path(f).exists()]
        missing = [f for f in creds_files if not Path(f).exists()]
        if missing:
            logging.warning(f"[!] Missing cred files (skipped): {missing}")

        if not self.creds_files:
            raise FileNotFoundError("No service account JSON files found!")

        self.exhausted = set()  # files that hit quota today
        self.idx = 0
        self._service = None
        self._current_file = None
        logging.info(f"[*] Loaded {len(self.creds_files)} service account(s)")

    def _build_service(self, creds_file: str):
        creds = service_account.Credentials.from_service_account_file(
            creds_file, scopes=SCOPES
        )
        return build("searchconsole", "v1", credentials=creds, cache_discovery=False)

    def get_service(self):
        """Returns current active service, building if needed."""
        if self._service is not None:
            return self._service

        # Find next non-exhausted account
        while self.idx < len(self.creds_files):
            f = self.creds_files[self.idx]
            if f not in self.exhausted:
                self._current_file = f
                self._service = self._build_service(f)
                logging.info(f"[*] Using service account: {f}")
                return self._service
            self.idx += 1

        return None  # all exhausted

    def mark_current_exhausted(self):
        """Mark current account as quota-exhausted and move to next."""
        if self._current_file:
            self.exhausted.add(self._current_file)
            logging.warning(f"[!] Quota exhausted: {self._current_file}")
        self._service = None
        self._current_file = None
        self.idx += 1

    def has_available(self) -> bool:
        return len(self.exhausted) < len(self.creds_files)


# ---------------------------------------------------------------------------
# Quota detection
# ---------------------------------------------------------------------------
def is_quota_error(e: HttpError) -> bool:
    """Detect quota/rate-limit errors vs temporary rate-limits."""
    if e.resp.status not in (429, 403):
        return False

    try:
        msg = str(e).lower()
    except Exception:
        msg = ""

    # Quota-exceeded keywords
    quota_markers = [
        "quota",
        "quotaexceeded",
        "daily limit",
        "rate limit exceeded",
        "ratelimitexceeded",
    ]
    return any(m in msg for m in quota_markers)


# ---------------------------------------------------------------------------
# Retry with rotation
# ---------------------------------------------------------------------------
def retry_with_rotation(rotator: ServiceRotator, func_builder, max_retries=MAX_RETRIES):
    """
    func_builder: callable that takes (service) and returns the actual API call result.
    On quota error → rotate service account and retry.
    On transient 429 → exponential backoff.
    """
    for attempt in range(max_retries + 1):
        service = rotator.get_service()
        if service is None:
            return False, Exception("All service accounts exhausted")

        try:
            return True, func_builder(service)

        except HttpError as e:
            if is_quota_error(e):
                logging.warning(f"    Quota error on current account: {e.resp.status}")
                rotator.mark_current_exhausted()
                if not rotator.has_available():
                    return False, Exception("All service accounts exhausted")
                # don't sleep — just swap and retry immediately
                continue

            if e.resp.status in (429, 500, 503) and attempt < max_retries:
                delay = BASE_DELAY * (2 ** attempt)
                logging.warning(f"    Transient error ({e.resp.status}). Retry {attempt+1}/{max_retries} in {delay}s...")
                time.sleep(delay)
                continue

            return False, e

        except requests.exceptions.RequestException as e:
            if attempt < max_retries:
                delay = BASE_DELAY * (2 ** attempt)
                logging.warning(f"    Network error. Retry {attempt+1}/{max_retries} in {delay}s...")
                time.sleep(delay)
                continue
            return False, e

        except Exception as e:
            return False, e

    return False, Exception("Max retries exceeded")


# ---------------------------------------------------------------------------
# Step 1: Fetch robots.txt → parse sitemaps
# ---------------------------------------------------------------------------
def fetch_sitemaps_from_robots() -> list[str]:
    robots_url = SITE_URL + "/robots.txt"
    logging.info(f"[*] Fetching {robots_url}")

    try:
        resp = requests.get(robots_url, timeout=15, headers={
            "User-Agent": "Mozilla/5.0 (GSC-Setup-Tool)"
        })
        resp.raise_for_status()
    except requests.RequestException as e:
        logging.error(f"[!] Could not fetch robots.txt: {e}")
        return []

    sitemaps = []
    for line in resp.text.splitlines():
        stripped = line.strip()
        if stripped.lower().startswith("sitemap:"):
            parts = stripped.split(None, 1)
            if len(parts) == 2 and parts[1].strip():
                sitemaps.append(parts[1].strip())

    logging.info(f"[+] Found {len(sitemaps)} sitemap(s) in robots.txt")
    return sitemaps


# ---------------------------------------------------------------------------
# Step 2: Submit sitemaps
# ---------------------------------------------------------------------------
def submit_sitemap(rotator: ServiceRotator, gsc_url: str, sitemap_url: str) -> bool:
    def _do(service):
        service.sitemaps().submit(siteUrl=gsc_url, feedpath=sitemap_url).execute()
        return True

    ok, result = retry_with_rotation(rotator, _do)
    if not ok:
        logging.error(f"    [!] Failed: {sitemap_url} — {result}")
    return ok


def submit_all_sitemaps(rotator: ServiceRotator, gsc_url: str, sitemaps: list[str]):
    total = len(sitemaps)
    logging.info(f"[*] Submitting {total} item(s)...")

    success = 0
    fail = 0
    for i, sm in enumerate(sitemaps, 1):
        logging.info(f"    [{i}/{total}] {sm}")

        if not rotator.has_available():
            logging.error("[!] All service accounts exhausted. Stopping submissions.")
            fail += (total - i + 1)
            break

        if submit_sitemap(rotator, gsc_url, sm):
            success += 1
        else:
            fail += 1
        time.sleep(DELAY_BETWEEN_SITEMAPS)

    logging.info(f"[+] Results: {success} submitted, {fail} failed")


# ---------------------------------------------------------------------------
# Step 3: Inspect home URL
# ---------------------------------------------------------------------------
def inspect_home(rotator: ServiceRotator, gsc_url: str):
    logging.info(f"[*] Inspecting: {gsc_url}")

    if not rotator.has_available():
        logging.error("[!] All service accounts exhausted. Skipping inspection.")
        return

    def _do(service):
        return service.urlInspection().index().inspect(
            body={"inspectionUrl": gsc_url, "siteUrl": gsc_url}
        ).execute()

    ok, result = retry_with_rotation(rotator, _do)
    if not ok:
        logging.error(f"[!] Inspection failed: {result}")
        return

    ix = result.get("inspectionResult", {})
    idx = ix.get("indexStatusResult", {})

    logging.info("[+] Inspection Result:")
    logging.info(f"    Verdict:          {idx.get('verdict', 'N/A')}")
    logging.info(f"    Coverage State:   {idx.get('coverageState', 'N/A')}")
    logging.info(f"    Crawl Timestamp:  {idx.get('crawlTimestamp', 'N/A')}")
    logging.info(f"    Indexing State:   {idx.get('indexingState', 'N/A')}")
    logging.info(f"    robots.txt State: {idx.get('robotsTxtState', 'N/A')}")
    logging.info(f"    Page Fetch State: {idx.get('pageFetchState', 'N/A')}")

    mobile = ix.get("mobileUsabilityResult", {})
    if mobile:
        logging.info(f"    Mobile Usability: {mobile.get('verdict', 'N/A')}")

    rich = ix.get("richResultsResult", {})
    if rich:
        logging.info(f"    Rich Results:     {rich.get('verdict', 'N/A')}")


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
    gsc_url = SITE_URL + "/"
    feed_url = SITE_URL + "/feed.xml"

    log_file = setup_logging()

    logging.info("=" * 60)
    logging.info(f"  brain.py — {SITE_URL}")
    logging.info(f"  {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    logging.info("=" * 60)

    rotator = ServiceRotator(GSC_CREDS_LIST)

    # ── Step 1: Fetch sitemaps from robots.txt ──
    logging.info("\n── Fetch Sitemaps ──")
    sitemaps = fetch_sitemaps_from_robots()

    if feed_url not in sitemaps:
        sitemaps.append(feed_url)
        logging.info(f"[+] Added: feed.xml")

    # ── Step 2: Submit everything ──
    logging.info("\n── Submit Sitemaps ──")
    if sitemaps:
        submit_all_sitemaps(rotator, gsc_url, sitemaps)
    else:
        logging.info("[!] Nothing to submit")

    # ── Step 3: Inspect home ──
    logging.info("\n── Inspect Home ──")
    inspect_home(rotator, gsc_url)

    # Summary
    logging.info("\n" + "=" * 60)
    logging.info(f"  Accounts used: {len(rotator.creds_files) - len(rotator.exhausted)}/{len(rotator.creds_files)}")
    if rotator.exhausted:
        logging.info(f"  Exhausted: {list(rotator.exhausted)}")
    logging.info(f"  Done! Log: {log_file}")
    logging.info("=" * 60)


if __name__ == "__main__":
    main()