"""Search SoldFetch sold listings, keep an audit trail, and export CSV."""
import argparse
import csv
import json
import os
import re
import time
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import HTTPRedirectHandler, Request, build_opener

SITE = "ebay.com"
KEYWORD = "sony wh-1000xm5"
CURRENCY = "USD"
COUNT = 200
MODEL = re.compile(r"\bwh[-\s]?1000xm5\b", re.I)
EXCLUDE = re.compile(
    r"\b(for parts|parts only|not working|broken|empty box|box only|"
    r"ear\s?pads?|replacement|case only|carrying case)\b", re.I
)
FIELDS = [
    "ebaySite", "itemId", "title", "url", "condition", "displayedPrice",
    "currency", "shippingPrice", "shippingCurrency", "totalPrice",
    "endedAt", "scrapedAt", "priceBasis", "saleDateVerified",
    "exactProductMatchVerified", "acceptedOfferAmount", "reviewReason",
]


class NoRedirect(HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


def fetch_page(page):
    key = os.environ.get("SOLDFETCH_API_KEY", "").strip()
    if not key:
        raise ValueError("Set SOLDFETCH_API_KEY before running this script.")
    params = {
        "keyword": KEYWORD, "ebaySite": SITE, "sold": "true",
        "itemCondition": "used", "count": COUNT, "page": page,
        "sortOrder": "endedRecently", "exactMatch": "true",
    }
    request = Request(
        "https://soldfetch.com/v1/scrape?" + urlencode(params),
        headers={"Authorization": "Bearer " + key, "Accept": "application/json"},
    )
    try:
        with build_opener(NoRedirect()).open(request, timeout=65) as response:
            data = json.load(response)
    except HTTPError as error:
        try:
            detail = json.loads(error.read())
        except (ValueError, UnicodeError):
            detail = {}
        raise RuntimeError(
            f"HTTP {error.code}; code={detail.get('code', 'unknown')}; "
            f"requestId={error.headers.get('x-request-id', 'unavailable')}"
        ) from None
    except (URLError, TimeoutError):
        raise RuntimeError("Network failure; check request history before retrying.") from None
    if not isinstance(data, dict) or not isinstance(data.get("items"), list):
        raise RuntimeError("Unexpected search response: expected top-level items.")
    return data


def collect(max_pages, get_page=fetch_page, checkpoint=None):
    if not 1 <= max_pages <= 3:
        raise ValueError("This tutorial permits one to three pages.")
    pages, seen = [], set()
    for page in range(1, max_pages + 1):
        data = get_page(page)
        pages.append(data)
        if checkpoint is not None:
            checkpoint(pages)
        keys = {str(row["itemId"]) for row in data["items"] if row.get("itemId")}
        if not keys - seen:
            break
        seen.update(keys)
        if data.get("hasNextPage") is False:
            break
        if page < max_pages:
            time.sleep(6.1)
    return pages


def clean(pages):
    candidates, review, seen = [], [], set()
    for page in pages:
        for item in page["items"]:
            item_id = str(item.get("itemId") or "")
            quality = item.get("quality") or {}
            reasons = []
            if not item_id:
                reasons.append("missing_item_id")
            elif (SITE, item_id) in seen:
                reasons.append("duplicate_item")
            seen.add((SITE, item_id))
            title = item.get("title") or ""
            if not MODEL.search(title):
                reasons.append("model_not_in_title")
            if EXCLUDE.search(title):
                reasons.append("accessory_or_parts_phrase")
            if item.get("listingType") != "sold":
                reasons.append("not_a_sold_listing")
            if item.get("soldCurrency") != CURRENCY:
                reasons.append("unexpected_currency")
            if quality.get("priceBasis") != "displayed_listing_price":
                reasons.append("unusable_price_basis")
            amount = None
            try:
                amount = Decimal(str(item.get("soldPrice")))
                if not amount.is_finite() or amount < 0:
                    raise ValueError
            except (InvalidOperation, ValueError):
                amount = None
                reasons.append("missing_or_invalid_price")
            row = {
                "ebaySite": SITE, "itemId": item_id, "title": title,
                "url": item.get("url"), "condition": item.get("condition"),
                "displayedPrice": format(amount, "f") if amount is not None else None,
                "currency": item.get("soldCurrency"),
                "shippingPrice": item.get("shippingPrice"),
                "shippingCurrency": item.get("shippingCurrency"),
                "totalPrice": item.get("totalPrice"),
                "endedAt": item.get("endedAt"),
                "scrapedAt": item.get("scrapedAt") or page.get("fetchedAt"),
                "priceBasis": quality.get("priceBasis"),
                "saleDateVerified": quality.get("saleDateVerified"),
                "exactProductMatchVerified": quality.get("exactProductMatchVerified"),
                "acceptedOfferAmount": quality.get("acceptedOfferAmount"),
                "reviewReason": ";".join(reasons),
            }
            (review if reasons else candidates).append(row)
    return candidates, review


def csv_cell(value):
    if value is None:
        return ""
    text = str(value)
    if text.lstrip().startswith(("=", "+", "-", "@")) or text.startswith(("\t", "\r", "\n")):
        return "'" + text
    return text


def write_csv(path, rows):
    with path.open("w", newline="", encoding="utf-8-sig") as handle:
        writer = csv.DictWriter(handle, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows({key: csv_cell(row.get(key)) for key in FIELDS} for row in rows)


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--pages", type=int, choices=(1, 2, 3), default=1)
    args = parser.parse_args()
    stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
    folder = Path("soldfetch-export-" + stamp)
    folder.mkdir()
    print(f"Saving results in {folder}")

    def checkpoint(pages):
        manifest = {"keyword": KEYWORD, "ebaySite": SITE, "count": COUNT, "responses": pages}
        (folder / "raw.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")

    pages = collect(args.pages, checkpoint=checkpoint)
    candidates, review = clean(pages)
    write_csv(folder / "candidates.csv", candidates)
    write_csv(folder / "review.csv", review)
    print(f"{len(pages)} successful pages; {len(candidates)} candidates; {len(review)} review rows")
    print(f"Files saved in {folder}")


if __name__ == "__main__":
    try:
        main()
    except (ValueError, RuntimeError, OSError) as error:
        raise SystemExit(str(error)) from None
