#!/usr/bin/env python3
"""Dated carbon or same-grade marine CSV; Python 3.9+, standard library only.

Set OILPRICEAPI_KEY, then run one of:
  python3 market_report.py carbon --output eu-carbon.csv
  python3 market_report.py marine --output singapore-rotterdam.csv
Files are replaced atomically only after every input passes validation.
"""
import argparse
import csv
import datetime as dt
import io
import json
import math
import os
from pathlib import Path
import sys
import tempfile
import urllib.error
import urllib.request

UTC = dt.timezone.utc


def timestamp(value, field):
    if not isinstance(value, str):
        raise ValueError(f"Missing {field}")
    parsed = dt.datetime.fromisoformat(value.replace("Z", "+00:00"))
    if parsed.tzinfo is None:
        raise ValueError(f"{field} must have a timezone")
    return parsed.astimezone(UTC)


def validate(payload, code, currency, unit, now):
    if not isinstance(payload, dict) or payload.get("status") != "success":
        raise ValueError("Expected a successful response")
    data = payload.get("data")
    if not isinstance(data, dict) or data.get("code") != code:
        raise ValueError("Missing or mismatched benchmark")
    raw = data.get("price")
    if raw is None or isinstance(raw, bool) or str(raw).strip() == "":
        raise ValueError("Missing price")
    price = float(raw)
    if not math.isfinite(price) or price <= 0:
        raise ValueError("Expected a positive finite price")
    if data.get("currency") != currency or data.get("unit") not in unit:
        raise ValueError("Currency or unit mismatch")
    recorded = timestamp(data.get("collected_at") or data.get("created_at"), "record timestamp")
    as_of = timestamp(data.get("as_of"), "assessment time") if data.get("as_of") else None
    effective = as_of or recorded
    if data.get("synthetic") is True or data.get("stale") is True or data.get("data_status") in ("stale", "unavailable", "discontinued"):
        raise ValueError("Synthetic, stale or unavailable data")
    if effective > now + dt.timedelta(minutes=5) or recorded > now + dt.timedelta(minutes=5) or now - effective > dt.timedelta(days=4):
        raise ValueError("Observation is future-dated or more than four days old")
    return {"code": code, "price": price, "currency": currency,
            "unit": data["unit"], "recorded": recorded.isoformat(),
            "as_of": as_of.isoformat() if as_of else "",
            "time_basis": "api_as_of" if as_of and as_of != recorded else "record_time_only"}


def fetch(code, key):
    request = urllib.request.Request(
        f"https://api.oilpriceapi.com/v1/prices/latest?by_code={code}",
        headers={"Authorization": f"Token {key}", "Accept": "application/json", "User-Agent": "OilPriceAPI-Market-Report/1.0"},
    )
    with urllib.request.urlopen(request, timeout=20) as response:
        return json.load(response)


def make_report(mode, key, now=None):
    now = now or dt.datetime.now(UTC)
    output = io.StringIO(newline="")
    writer = csv.writer(output)
    if mode == "carbon":
        row = validate(fetch("EU_CARBON_EUR", key), "EU_CARBON_EUR", "EUR", ("metric_ton_co2", "tCO2"), now)
        writer.writerow(["exported_at_utc", "code", "eur_per_tco2", "api_as_of_utc", "recorded_at_utc", "time_basis", "series_type"])
        writer.writerow([now.isoformat(), row["code"], row["price"], row["as_of"], row["recorded"], row["time_basis"], "aggregated_market_reading"])
    else:
        rows = [validate(fetch(code, key), code, "USD", ("metric_ton",), now)
                for code in ("VLSFO_SGSIN_USD", "VLSFO_NLRTM_USD")]
        if not all(row["as_of"] for row in rows):
            raise ValueError("Both marine assessment dates are required for comparison")
        if rows[0]["as_of"][:10] != rows[1]["as_of"][:10]:
            raise ValueError("Marine assessment dates differ; no comparison was written")
        writer.writerow(["exported_at_utc", "grade", "unit", "singapore_usd", "rotterdam_usd", "singapore_minus_rotterdam_usd", "singapore_as_of_utc", "rotterdam_as_of_utc", "singapore_recorded_at_utc", "rotterdam_recorded_at_utc"])
        writer.writerow([now.isoformat(), "VLSFO", "metric_ton", rows[0]["price"], rows[1]["price"], round(rows[0]["price"] - rows[1]["price"], 6), rows[0]["as_of"], rows[1]["as_of"], rows[0]["recorded"], rows[1]["recorded"]])
    return output.getvalue()


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("report", choices=("carbon", "marine"))
    parser.add_argument("--output", type=Path, required=True)
    args = parser.parse_args(argv)
    key = os.environ.get("OILPRICEAPI_KEY", "").strip()
    if not key:
        print("Set OILPRICEAPI_KEY to your account API key. No file was changed.", file=sys.stderr)
        return 1
    temporary = None
    try:
        report = make_report(args.report, key)
        with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="", dir=args.output.parent, delete=False) as handle:
            temporary = handle.name
            handle.write(report)
        os.replace(temporary, args.output)
        temporary = None
    except urllib.error.HTTPError as error:
        recovery = {401: "Check your API key.", 402: "Check dataset entitlement in your account.", 403: "Check dataset entitlement and email confirmation.", 429: "Wait for your quota to reset before retrying."}
        print(f"HTTP {error.code}. {recovery.get(error.code, 'Retry later.')} No file was changed.", file=sys.stderr)
        return 1
    except (OSError, ValueError, TypeError) as error:
        # Do not print arbitrary upstream bodies or URLs containing credentials.
        detail = str(error) if isinstance(error, ValueError) else type(error).__name__
        print(f"Report unavailable: {detail}. Retry after checking the data and output location. No file was changed.", file=sys.stderr)
        return 1
    finally:
        if temporary:
            Path(temporary).unlink(missing_ok=True)
    print(f"Saved {args.output}. Check dates before using this snapshot.", file=sys.stderr)
    return 0


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