#!/usr/bin/env python3
"""Start a Hubertino scrape, poll until it finishes, download the CSV.

    pip install -r requirements.txt
    export HUBERTINO_API_KEY=hub_live_...          # Dashboard -> Settings
    python scrape.py --category dentist --location "Austin, TX, United States" \
        --max-results 50 --out leads.csv

Optional:
    --jsonl rows.jsonl   also page through /results and write one JSON row per line
    HUBERTINO_BASE_URL   default https://hubertino.com/api/v1
    HUBERTINO_POLL_SECONDS  default 5
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
from typing import Iterator

import requests

BASE_URL = os.environ.get("HUBERTINO_BASE_URL", "https://hubertino.com/api/v1").rstrip("/")
POLL_SECONDS = float(os.environ.get("HUBERTINO_POLL_SECONDS", "5"))


class HubertinoError(Exception):
    """An API error: every error body is JSON of the form {"error": "..."}."""

    def __init__(self, status: int, message: str):
        super().__init__(f"HTTP {status}: {message}")
        self.status = status


class ScrapeFailed(Exception):
    """The scrape ended with status "error". Its reserved credits were refunded."""


class Hubertino:
    def __init__(self, api_key: str, base_url: str = BASE_URL):
        if not api_key.startswith("hub_live_"):
            raise ValueError("API keys start with hub_live_ (Dashboard -> Settings)")
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers["Authorization"] = f"Bearer {api_key}"

    def _request(self, method: str, path: str, **kwargs) -> requests.Response:
        res = self.session.request(method, f"{self.base_url}{path}", timeout=120, **kwargs)
        if res.status_code >= 400:
            try:
                message = res.json().get("error", res.text)
            except ValueError:
                message = res.text
            raise HubertinoError(res.status_code, message)
        return res

    def start_scrape(self, categories: list[str], locations: list[str], max_results: int,
                     country: str | None = None) -> dict:
        body = {"categories": categories, "locations": locations, "maxResults": max_results}
        if country:
            body["country"] = country  # else inferred from the locations, falling back to "us"
        return self._request("POST", "/scrapes", json=body).json()["scrape"]

    def get_scrape(self, scrape_id: str) -> dict:
        return self._request("GET", f"/scrapes/{scrape_id}").json()["scrape"]

    def wait(self, scrape_id: str, poll_seconds: float = POLL_SECONDS) -> dict:
        """Poll until status is done (returns the scrape) or error (raises)."""
        while True:
            scrape = self.get_scrape(scrape_id)
            if scrape["status"] == "done":
                return scrape
            if scrape["status"] == "error":
                raise ScrapeFailed(scrape.get("error") or "The scrape failed.")
            percent = (scrape.get("progress") or {}).get("percent", 0)
            print(f"  {scrape['status']} {percent}% · {scrape['resultCount']} places", file=sys.stderr)
            time.sleep(poll_seconds)

    def iter_rows(self, scrape_id: str, page_size: int = 1000) -> Iterator[dict]:
        """Page through /results (limit <= 5000) until hasMore is false."""
        offset = 0
        while True:
            page = self._request(
                "GET", f"/scrapes/{scrape_id}/results",
                params={"limit": page_size, "offset": offset},
            ).json()
            yield from page["rows"]
            if not page["hasMore"] or not page["rows"]:
                return
            offset += len(page["rows"])

    def download_export(self, scrape_id: str, path: str, fmt: str = "csv") -> None:
        """Save the CSV/XLSX export. Only available once the scrape is done (else 409)."""
        res = self._request("GET", f"/scrapes/{scrape_id}/export", params={"format": fmt}, stream=True)
        with open(path, "wb") as fh:
            for chunk in res.iter_content(chunk_size=65536):
                fh.write(chunk)


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("--category", action="append", required=True, help="repeat for several")
    ap.add_argument("--location", action="append", required=True, help="repeat for several")
    ap.add_argument("--max-results", type=int, default=50, help="per category x location (1-500)")
    ap.add_argument("--country", help="Google country edition, e.g. us, gb, de (optional)")
    ap.add_argument("--out", default="leads.csv", help="CSV path (default leads.csv)")
    ap.add_argument("--jsonl", help="also write every row as JSON lines to this path")
    args = ap.parse_args()

    api_key = os.environ.get("HUBERTINO_API_KEY")
    if not api_key:
        print("Set HUBERTINO_API_KEY to your hub_live_... key", file=sys.stderr)
        return 2

    try:
        client = Hubertino(api_key)
        scrape = client.start_scrape(args.category, args.location, args.max_results, args.country)
        print(f"Started scrape {scrape['id']} (reserved {scrape['creditsReserved']} credits)", file=sys.stderr)
        scrape = client.wait(scrape["id"])
        print(f"Done: {scrape['resultCount']} places, {scrape['creditsCharged']} credits charged", file=sys.stderr)
        client.download_export(scrape["id"], args.out, "csv")
        print(f"Saved {args.out}", file=sys.stderr)
        if args.jsonl:
            n = 0
            with open(args.jsonl, "w", encoding="utf-8") as fh:
                for row in client.iter_rows(scrape["id"]):
                    fh.write(json.dumps(row, ensure_ascii=False) + "\n")
                    n += 1
            print(f"Saved {n} rows to {args.jsonl}", file=sys.stderr)
    except (HubertinoError, ScrapeFailed, ValueError) as err:
        print(err, file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
