"""Enrich addresses.csv (id,address) into properties.csv and properties.json.

Requires Python 3.10+ and requests. Set PROPERTIESAPI_KEY in your environment.
No automatic retries: inspect failed or uncertain rows before trying again.
"""
import argparse
import csv
import json
import os
from datetime import datetime, timezone
from pathlib import Path

import requests

ENDPOINT = "https://api.propertiesapi.com/v1/zillow/property"
FIELDS = ["input_id", "input_address", "status", "error_code", "zpid", "address",
          "unit", "home_status", "currency", "price", "zestimate", "rent_zestimate",
          "beds", "baths", "living_area", "fetched_at", "cached", "credits",
          "review_required"]


def spreadsheet_cell(value):
    if isinstance(value, str) and (value.lstrip().startswith(("=", "+", "-", "@"))
                                  or value.startswith(("\t", "\r", "\n"))):
        return "'" + value
    return value


def load_inputs(path):
    with Path(path).open(newline="", encoding="utf-8-sig") as source:
        reader = csv.DictReader(source)
        if not {"id", "address"}.issubset(reader.fieldnames or []):
            raise ValueError("Input needs id and address columns")
        rows = list(reader)
    seen = set()
    for row in rows:
        row["id"] = (row.get("id") or "").strip()
        row["address"] = (row.get("address") or "").strip()
        if not row["id"] or not row["address"] or row["id"] in seen:
            raise ValueError("Every row needs an address and a unique, nonempty id")
        seen.add(row["id"])
    if not rows:
        raise ValueError("Input has no property rows")
    return rows


def enrich(inputs, api_key, endpoint=ENDPOINT):
    rows, stop_reason = [], None
    with requests.Session() as session:
        session.headers["x-api-key"] = api_key
        for source in inputs:
            row = dict.fromkeys(FIELDS)
            row.update(input_id=source["id"], input_address=source["address"],
                       status="error", review_required=True)
            rows.append(row)
            if stop_reason:
                row.update(status="not_requested", error_code=stop_reason)
                continue
            try:
                response = session.get(endpoint, params={"address": source["address"]},
                                       timeout=(10, 140), allow_redirects=False)
            except requests.RequestException:
                row.update(status="uncertain", error_code="transport_error")
                stop_reason = "stopped_after_transport_error"
                continue
            try:
                body = response.json()
                if not isinstance(body, dict):
                    raise ValueError("Unexpected response")
            except ValueError:
                row.update(status="uncertain", error_code="invalid_response")
                stop_reason = "stopped_after_invalid_response"
                continue
            if response.status_code != 200:
                error = body.get("error")
                code = error.get("code") if isinstance(error, dict) else None
                row["error_code"] = str(code or f"http_{response.status_code}")
                if response.status_code in (401, 402, 429) or response.status_code >= 500:
                    stop_reason = "stopped_after_" + row["error_code"]
                continue
            data, meta = body.get("data"), body.get("meta")
            if not isinstance(data, dict) or not isinstance(meta, dict):
                row.update(status="uncertain", error_code="invalid_response")
                stop_reason = "stopped_after_invalid_response"
                continue
            row.update(status="ok", zpid=data.get("zpid"), address=data.get("address"),
                       unit=data.get("unit"), home_status=data.get("homeStatus"),
                       currency=data.get("currency"), price=data.get("price"),
                       zestimate=data.get("zestimate"), rent_zestimate=data.get("rentZestimate"),
                       beds=data.get("beds"), baths=data.get("baths"), living_area=data.get("livingArea"),
                       fetched_at=meta.get("fetched_at"), cached=meta.get("cached"),
                       credits=meta.get("credits"),
                       review_required=bool(meta.get("warning") or not data.get("zpid")
                                            or not data.get("address")))
    return rows


def save(rows, output_directory):
    destination = Path(output_directory)
    destination.mkdir(parents=True, exist_ok=True)
    with (destination / "properties.csv").open("w", newline="", encoding="utf-8") as target:
        writer = csv.DictWriter(target, fieldnames=FIELDS)
        writer.writeheader()
        writer.writerows({k: spreadsheet_cell(v) for k, v in row.items()} for row in rows)
    snapshot = {"exported_at": datetime.now(timezone.utc).isoformat(), "rows": rows}
    (destination / "properties.json").write_text(json.dumps(snapshot, indent=2), encoding="utf-8")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input", help="CSV with id,address columns")
    parser.add_argument("--output-dir", default="output")
    args = parser.parse_args()
    api_key = os.environ.get("PROPERTIESAPI_KEY", "").strip()
    if not api_key:
        parser.error("Set PROPERTIESAPI_KEY before running")
    try:
        inputs = load_inputs(args.input)
    except (ValueError, OSError) as error:
        parser.error(str(error))
    rows = enrich(inputs, api_key)
    save(rows, args.output_dir)
    print(f"Saved {len(rows)} rows; {sum(r['status'] == 'ok' for r in rows)} returned properties.")
    return 0 if all(r["status"] == "ok" and not r["review_required"] for r in rows) else 1


if __name__ == "__main__":
    raise SystemExit(main())
