#!/usr/bin/env python3
"""Retry failed alignment/index probes slowly and rewrite the audit CSV.

This is deliberately a low-concurrency second pass.  Large public archives can
temporarily return 403/503 responses or a small error document under load.
"""

from __future__ import annotations

import csv
import json
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path

from audit_public_wes_alignments import https_from_ftpish, range_probe


def as_int(value: object) -> int | None:
    text = str(value or "").strip()
    return int(text) if text.isdigit() else None


def truthy(value: object) -> bool:
    return str(value).strip().lower() in {"1", "true", "yes"}


def range_class(status: object) -> str:
    if str(status) == "206":
        return "HTTP_206_range_honored"
    if str(status) == "200":
        return "HTTP_200_range_ignored"
    return "probe_failed"


def main() -> int:
    if len(sys.argv) not in (2, 3):
        raise SystemExit("usage: retry_failed_alignment_pairs.py CSV [WORKERS]")
    path = Path(sys.argv[1])
    workers = int(sys.argv[2]) if len(sys.argv) == 3 else 2
    with path.open("r", encoding="utf-8", newline="") as fh:
        reader = csv.DictReader(fh)
        original_fields = list(reader.fieldnames or [])
        rows = list(reader)

    pending = [i for i, row in enumerate(rows) if not truthy(row.get("verified_downloadable"))]
    tasks = {}
    started = time.time()
    with ThreadPoolExecutor(max_workers=workers) as pool:
        for i in pending:
            row = rows[i]
            for kind in ("alignment", "index"):
                url_key = f"{kind}_url"
                bytes_key = f"{kind}_bytes"
                corrected = https_from_ftpish(row[url_key])
                row[url_key] = corrected
                tasks[pool.submit(range_probe, corrected, as_int(row.get(bytes_key)), 8)] = (i, kind)
        done = ok = 0
        for future in as_completed(tasks):
            i, kind = tasks[future]
            result = future.result()
            row = rows[i]
            row[f"{kind}_http_status"] = str(result.get("status") or "")
            row[f"{kind}_observed_bytes"] = str(result.get("observed_total_bytes") or "")
            row[f"{kind}_content_range"] = str(result.get("content_range") or "")
            row[f"{kind}_probe_bytes_received"] = str(result.get("bytes_received") or 0)
            row[f"{kind}_range_result"] = range_class(result.get("status"))
            row[f"{kind}_probe_ok"] = str(bool(result.get("ok")))
            row[f"{kind}_probe_attempts"] = str(result.get("attempts") or "")
            row[f"{kind}_probe_error"] = str(result.get("error") or "")
            done += 1
            ok += int(bool(result.get("ok")))
            if done % 20 == 0 or done == len(tasks):
                print(json.dumps({"done": done, "total": len(tasks), "ok": ok}), flush=True)

    for row in rows:
        if row.get("alignment_probe_ok") or row.get("index_probe_ok"):
            row["verified_downloadable"] = str(
                truthy(row.get("alignment_probe_ok")) and truthy(row.get("index_probe_ok"))
            )
        else:
            # Preserve successful first-pass records and add normalized status labels.
            row["alignment_range_result"] = range_class(row.get("alignment_http_status"))
            row["index_range_result"] = range_class(row.get("index_http_status"))
            row["alignment_probe_ok"] = str(truthy(row.get("verified_downloadable")))
            row["index_probe_ok"] = str(truthy(row.get("verified_downloadable")))

    added_fields = [
        "alignment_content_range", "index_content_range",
        "alignment_probe_bytes_received", "index_probe_bytes_received",
        "alignment_range_result", "index_range_result",
        "alignment_probe_ok", "index_probe_ok",
        "alignment_probe_attempts", "index_probe_attempts",
        "alignment_probe_error", "index_probe_error",
    ]
    fields = original_fields + [field for field in added_fields if field not in original_fields]
    temporary = path.with_suffix(path.suffix + ".retry.tmp")
    with temporary.open("w", encoding="utf-8", newline="") as fh:
        writer = csv.DictWriter(fh, fieldnames=fields, extrasaction="ignore")
        writer.writeheader()
        writer.writerows(rows)
    temporary.replace(path)

    payload = {
        "completed_at": datetime.now(timezone.utc).isoformat(),
        "csv": str(path),
        "workers": workers,
        "pairs_retried": len(pending),
        "probe_tasks": len(tasks),
        "verified_pairs_after_retry": sum(truthy(row.get("verified_downloadable")) for row in rows),
        "failed_pairs_after_retry": sum(not truthy(row.get("verified_downloadable")) for row in rows),
        "elapsed_seconds": round(time.time() - started, 1),
    }
    report = path.with_name(path.stem + "_retry_report.json")
    report.write_text(json.dumps(payload, indent=2), encoding="utf-8")
    print(json.dumps(payload), flush=True)
    return 0


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