import csv
import requests
import time
import threading
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

# ====================================================
# Optional tqdm progress bar
# ====================================================
try:
    from tqdm import tqdm
    HAS_TQDM = True
except:
    HAS_TQDM = False

# ====================================================
# CONFIG
# ====================================================
API_URL = "https://inp.api-us1.com/api/3"
API_KEY = "0493c4d2d33662a670c7b347077c74dce411fe0261fb00affa7e8cdd34041ec16861355a"   # <<< replace this

base = Path(__file__).resolve().parent
CSV_FILE = base / "Contacts_Matched_test.csv"
FAILED_LOG_FILE = base / "failed_updates.csv"

# AC Retry Codes
RETRY_STATUS = {429, 500, 502, 503, 504}
MAX_RETRIES = 5

EMAIL_COL = "Email"

CUSTOM_FIELD_IDS = {
    "id": 17, "cancel date": 79, "newmbr date": 86, "paid thru": 264,
    "co id": 16, "cert status": 183, "pcl status": 287,
    "pcl completion date": 289, "pf1 status": 291,
    "pf1 completion date": 292, "pf2 status": 293,
    "pf2 completion date": 294, "intro to accounting status": 295,
    "intro to accounting completion date": 296, "state province": 4,
    "c p a   certification": 182, "last  act": 278, "french": 7,
    "last  infoline": 276, "last conf": 277,
    "Processed member type": 18, "new lead status": 2,
    "New DO ID": 20, "Number of Passed Courses": 297
}

HEADERS = {
    "Api-Token": API_KEY,
    "Content-Type": "application/json",
}

# ====================================================
# RATE LIMITER (token bucket: max 5 requests/sec)
# ====================================================
MAX_CALLS_PER_SECOND = 5
rate_limiter = threading.Semaphore(MAX_CALLS_PER_SECOND)

def refill_tokens():
    """Refill 5 tokens every second globally (not per thread)."""
    while True:
        while rate_limiter._value < MAX_CALLS_PER_SECOND:
            rate_limiter.release()
        time.sleep(1)

# Start the refill thread
threading.Thread(target=refill_tokens, daemon=True).start()

# ====================================================
# SAFE REQUEST WITH RETRIES + RATE LIMITING
# ====================================================
def safe_request(method, url, **kwargs):
    for attempt in range(MAX_RETRIES):

        rate_limiter.acquire()  # limit RPS

        try:
            r = requests.request(method, url, headers=HEADERS, timeout=20, **kwargs)
        except:
            time.sleep(1)
            continue

        if r.status_code in RETRY_STATUS:
            time.sleep(1.2 * (attempt + 1))
            continue

        return r

    return r  # last attempt

# ====================================================
# HELPERS
# ====================================================
def _safe_date(v):
    if not v or not v.strip():
        return ""
    s = v.strip()
    for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%m/%d/%Y", "%d/%m/%Y"):
        try:
            return datetime.strptime(s, fmt).strftime("%Y-%m-%d")
        except:
            pass
    return s

# ====================================================
# CONTACT LOOKUP + UPDATE
# ====================================================
contact_id_cache = {}

def fetch_contact_id(email):
    r = safe_request("GET", f"{API_URL}/contacts", params={"email": email})
    if r.status_code != 200:
        return None
    contacts = r.json().get("contacts", [])
    return contacts[0]["id"] if contacts else None

def update_contact(cid, core, custom):
    payload = {"contact": core}
    if custom:
        payload["contact"]["fieldValues"] = custom
    return safe_request("PUT", f"{API_URL}/contacts/{cid}", json=payload)

# ====================================================
# THREAD WORKER
# ====================================================
def process_row(row):
    email = (row.get(EMAIL_COL, "") or "").strip().lower()
    if not email:
        return ("skip", email)

    cid = contact_id_cache.get(email)
    if not cid:
        return ("skip", email)

    # Build update payload
    core = {"email": email}
    custom = []

    for col, fid in CUSTOM_FIELD_IDS.items():
        val = (row.get(col, "") or "").strip()
        if val:
            if col.lower() == "paid thru":
                val = _safe_date(val)
            custom.append({"field": str(fid), "value": val})

    # Send update
    r = update_contact(cid, core, custom)
    if r.status_code not in (200, 201):
        return ("fail", email, r.status_code, row)

    return ("ok", email)

# ====================================================
# MAIN PROGRAM
# ====================================================
if __name__ == "__main__":

    print("📥 Loading CSV...")
    with open(CSV_FILE, newline='', encoding="utf-8-sig") as f:
        rows = list(csv.DictReader(f))

    total = len(rows)
    print(f"Found {total:,} rows.")

    # -----------------------------------
    # Prefetch Contact IDs in bulk
    # -----------------------------------
    print("📡 Fetching contact IDs...")
    unique_emails = {r.get(EMAIL_COL, "").strip().lower() for r in rows if r.get(EMAIL_COL)}

    iterator = tqdm(unique_emails) if HAS_TQDM else unique_emails
    for email in iterator:
        contact_id_cache[email] = fetch_contact_id(email)

    # -----------------------------------
    # Multithreaded update engine
    # -----------------------------------
    print("\n🛠 Updating contacts with multithreading...")

    failures = []
    ok = skip = fail = 0

    NUM_THREADS = 4   # Safe on local machine; 10 threads = ~4× speed

    with ThreadPoolExecutor(max_workers=NUM_THREADS) as executor:
        futures = [executor.submit(process_row, row) for row in rows]

        iterator = tqdm(as_completed(futures), total=total) if HAS_TQDM else as_completed(futures)

        for f in iterator:
            res = f.result()

            if res[0] == "ok":
                ok += 1
            elif res[0] == "skip":
                skip += 1
            else:
                fail += 1
                failures.append({
                    "email": res[1],
                    "error": res[2],
                    **res[3]
                })

    # -----------------------------------
    # Save failures
    # -----------------------------------
    if failures:
        with open(FAILED_LOG_FILE, "w", newline='', encoding="utf-8-sig") as f:
            writer = csv.DictWriter(f, fieldnames=failures[0].keys())
            writer.writeheader()
            writer.writerows(failures)
        print(f"\n❌ Logged {len(failures)} failed updates → {FAILED_LOG_FILE}")

    print("\n==============================")
    print(f"✅ Success: {ok}")
    print(f"⚠️ Skipped: {skip}")
    print(f"❌ Failed: {fail}")
    print("==============================")
    print("🎉 Multithreaded update complete!")