#!/usr/bin/env python3
"""Check a daily Search Console property-total CSV locally, without network calls.
Not a Google connector or an audit of data collection. See README.txt.
"""
import argparse
import csv
import datetime as dt
import json
import math
from pathlib import Path


def check_report(path, start, end):
    if end < start:
        raise ValueError("End date must be on or after start date")
    expected = {(start + dt.timedelta(days=i)).isoformat() for i in range((end-start).days+1)}
    dates, rows = set(), []
    with Path(path).open(newline="", encoding="utf-8-sig") as handle:
        reader = csv.DictReader(handle)
        if not reader.fieldnames:
            raise ValueError("CSV needs a header")
        headers = {key.strip().lower(): key for key in reader.fieldnames}
        if len(headers) != len(reader.fieldnames):
            raise ValueError("CSV has duplicate column names")
        if not {"date", "clicks", "impressions", "position"}.issubset(headers):
            raise ValueError("Required columns: Date, Clicks, Impressions, Position")
        for line, row in enumerate(reader, 2):
            try:
                date = dt.date.fromisoformat(row[headers["date"]].strip()).isoformat()
                if date in dates:
                    raise ValueError("Duplicate date; use one property-total row per day")
                if date not in expected:
                    raise ValueError("Date outside requested period")
                clicks = float(row[headers["clicks"]].replace(",", ""))
                impressions = float(row[headers["impressions"]].replace(",", ""))
                position = float(row[headers["position"]].replace(",", ""))
                if not all(math.isfinite(v) and v >= 0 for v in [clicks, impressions, position]):
                    raise ValueError("Metrics must be finite and non-negative")
                if not clicks.is_integer() or not impressions.is_integer():
                    raise ValueError("Clicks and impressions must be whole counts")
                if impressions == 0 and clicks > 0:
                    raise ValueError("Clicks without impressions need review")
                if impressions > 0 and position < 1:
                    raise ValueError("Positive impressions need a position of at least 1")
            except (AttributeError, TypeError, ValueError) as exc:
                raise ValueError(f"Row {line}: {exc}") from exc
            dates.add(date)
            rows.append((int(clicks), int(impressions), position))
    missing = sorted(expected-dates)
    if missing:
        raise ValueError("Missing dates; do not replace them with zero: " + ", ".join(missing))
    clicks = sum(row[0] for row in rows)
    impressions = sum(row[1] for row in rows)
    return {"scope": "Checks on supplied daily property-total CSV only; source collection not verified", "start": start.isoformat(), "end": end.isoformat(), "rows": len(rows), "clicks": clicks, "impressions": impressions, "ctr": clicks/impressions if impressions else None, "impression_weighted_position": sum(row[1]*row[2] for row in rows)/impressions if impressions else None}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("csv", type=Path)
    parser.add_argument("--start", required=True, type=dt.date.fromisoformat)
    parser.add_argument("--end", required=True, type=dt.date.fromisoformat)
    args = parser.parse_args()
    try:
        print(json.dumps(check_report(args.csv, args.start, args.end), indent=2, allow_nan=False))
    except (ValueError, OSError, csv.Error) as exc:
        parser.exit(1, f"Check failed: {exc}\n")


if __name__ == "__main__":
    main()
