#!/usr/bin/env python3
"""Batch 1 user detail puller for Cascades Tucson M365 tenant."""
import csv
import json
import subprocess
import sys
import time
import urllib.parse
import urllib.request

TENANT_ID = "207fa277-e9d8-4eb7-ada1-1064d2221498"

USERS = [
    ("Allison.Reibschied@cascadestucson.com", "User"),
    ("Training@cascadestucson.com", "User"),
    ("accounting@cascadestucson.com", "Shared"),  # flagged shared but still licensed
    ("accountingassistant@cascadestucson.com", "User"),
    ("alyssa.brooks@cascadestucson.com", "User"),
]

OUTPUT_CSV = r"C:\claudetools\clients\cascades-tucson\reports\user-detail-batches\batch-1.csv"


def get_token():
    # Token is cached by get-token.sh at this path (TTL 55 min)
    cache_path = f"/tmp/remediation-tool/{TENANT_ID}/graph.jwt"
    # Try Windows-style path first since Python may not resolve /tmp the same way
    import os
    candidates = [
        cache_path,
        f"C:/Users/howar/AppData/Local/Temp/remediation-tool/{TENANT_ID}/graph.jwt",
        os.path.expandvars(rf"%TEMP%\remediation-tool\{TENANT_ID}\graph.jwt"),
    ]
    for p in candidates:
        try:
            with open(p, "r") as f:
                tok = f.read().strip()
            if tok.startswith("eyJ"):
                return tok
        except FileNotFoundError:
            continue
    raise RuntimeError(f"No token found in any of {candidates}")


def graph_get(token, path, extra_headers=None, max_retries=5):
    if path.startswith("http"):
        url = path
    else:
        url = f"https://graph.microsoft.com{path}"
    for attempt in range(max_retries):
        req = urllib.request.Request(url)
        req.add_header("Authorization", f"Bearer {token}")
        if extra_headers:
            for k, v in extra_headers.items():
                req.add_header(k, v)
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                return resp.status, json.loads(resp.read().decode("utf-8"))
        except urllib.error.HTTPError as e:
            try:
                body = json.loads(e.read().decode("utf-8"))
            except Exception:
                body = {"error_raw": str(e)}
            # Retry on 429 and 5xx
            if e.code == 429 or (500 <= e.code < 600):
                # Honor Retry-After if present
                retry_after = 0
                try:
                    retry_after = int(e.headers.get("Retry-After", "0"))
                except Exception:
                    retry_after = 0
                delay = max(retry_after, 2 ** attempt * 5)  # 5,10,20,40,80
                print(f"    [retry {attempt+1}/{max_retries}] HTTP {e.code}, sleeping {delay}s", file=sys.stderr)
                time.sleep(delay)
                continue
            return e.code, body
        except Exception as e:
            if attempt < max_retries - 1:
                time.sleep(5)
                continue
            return 0, {"error_exc": str(e)}
    return 429, {"error": "max_retries_exceeded"}


# Map OData auth method types to short names
METHOD_SHORT = {
    "#microsoft.graph.microsoftAuthenticatorAuthenticationMethod": "Authenticator",
    "#microsoft.graph.phoneAuthenticationMethod": "Phone",
    "#microsoft.graph.passwordAuthenticationMethod": "Password",
    "#microsoft.graph.fido2AuthenticationMethod": "FIDO2",
    "#microsoft.graph.windowsHelloForBusinessAuthenticationMethod": "WindowsHello",
    "#microsoft.graph.temporaryAccessPassAuthenticationMethod": "TAP",
    "#microsoft.graph.softwareOathAuthenticationMethod": "OATH",
    "#microsoft.graph.emailAuthenticationMethod": "Email",
    "#microsoft.graph.passwordlessMicrosoftAuthenticatorAuthenticationMethod": "PasswordlessAuthenticator",
    "#microsoft.graph.platformCredentialAuthenticationMethod": "PlatformCredential",
    "#microsoft.graph.hardwareOathAuthenticationMethod": "HardwareOATH",
}


def short_method(odata_type):
    if odata_type in METHOD_SHORT:
        return METHOD_SHORT[odata_type]
    # Strip the prefix and AuthenticationMethod suffix
    s = odata_type.replace("#microsoft.graph.", "").replace("AuthenticationMethod", "")
    return s[:1].upper() + s[1:] if s else odata_type


def process_user(token, upn, mailbox_type):
    row = {
        "UPN": upn,
        "DisplayName": "",
        "AccountEnabled": "",
        "MailboxType": mailbox_type,
        "Licenses": "",
        "MFARegistered": "",
        "MFAMethods": "",
        "DefaultMFAMethod": "",
        "LastSignIn": "",
        "LastInteractiveSignIn": "",
        "AdminRoles": "",
        "JobTitle": "",
        "Department": "",
        "GroupCount": "",
        "Notes": "",
    }
    notes = []

    upn_enc = urllib.parse.quote(upn)

    # 1a. Core profile (without signInActivity — that query is heavily throttled separately)
    status, data = graph_get(
        token,
        f"/v1.0/users/{upn_enc}?$select=id,userPrincipalName,displayName,accountEnabled,jobTitle,department",
    )
    if status == 404:
        row["Notes"] = "not_found"
        return row
    if status != 200:
        notes.append(f"profile_err_{status}")
    else:
        row["DisplayName"] = data.get("displayName") or ""
        row["AccountEnabled"] = str(data.get("accountEnabled", "")).lower()
        row["JobTitle"] = data.get("jobTitle") or ""
        row["Department"] = data.get("department") or ""

    # 1b. signInActivity (separate call — subject to distinct throttling)
    status, data = graph_get(
        token,
        f"/v1.0/users/{upn_enc}?$select=signInActivity",
    )
    if status == 200:
        sia = data.get("signInActivity") or {}
        row["LastSignIn"] = sia.get("lastSignInDateTime") or ""
        row["LastInteractiveSignIn"] = (
            sia.get("lastSignInDateTime") or sia.get("lastNonInteractiveSignInDateTime") or ""
        )
    elif status == 403:
        row["LastSignIn"] = "scope_unavailable"
        row["LastInteractiveSignIn"] = "scope_unavailable"
    else:
        notes.append(f"signin_err_{status}")

    # 2. Licenses
    status, data = graph_get(token, f"/v1.0/users/{upn_enc}/licenseDetails")
    if status == 200:
        skus = [v.get("skuPartNumber", "") for v in data.get("value", [])]
        row["Licenses"] = ";".join([s for s in skus if s])
    else:
        notes.append(f"lic_err_{status}")

    # 3. MFA methods
    status, data = graph_get(token, f"/v1.0/users/{upn_enc}/authentication/methods")
    if status == 200:
        methods = []
        non_pw = False
        for m in data.get("value", []):
            t = m.get("@odata.type", "")
            short = short_method(t)
            methods.append(short)
            if t != "#microsoft.graph.passwordAuthenticationMethod":
                non_pw = True
        row["MFAMethods"] = ";".join(methods)
        row["MFARegistered"] = "true" if non_pw else "false"
    elif status == 403:
        row["MFAMethods"] = "scope_unavailable"
        row["MFARegistered"] = "scope_unavailable"
    else:
        notes.append(f"mfa_err_{status}")

    # 4. Default MFA method (beta)
    status, data = graph_get(token, f"/beta/users/{upn_enc}/authentication/signInPreferences")
    if status == 200:
        row["DefaultMFAMethod"] = data.get("userPreferredMethodForSecondaryAuthentication") or ""
    elif status == 403:
        row["DefaultMFAMethod"] = "scope_unavailable"
    else:
        notes.append(f"defmfa_err_{status}")

    # 5. Directory roles
    status, data = graph_get(
        token, f"/v1.0/users/{upn_enc}/transitiveMemberOf/microsoft.graph.directoryRole"
    )
    if status == 200:
        roles = [r.get("displayName", "") for r in data.get("value", [])]
        row["AdminRoles"] = ";".join([r for r in roles if r])
    elif status == 403:
        row["AdminRoles"] = "scope_unavailable"
    else:
        notes.append(f"roles_err_{status}")

    # 6. Group count
    status, data = graph_get(
        token,
        f"/v1.0/users/{upn_enc}/memberOf?$count=true&$top=1",
        extra_headers={"ConsistencyLevel": "eventual"},
    )
    if status == 200:
        count = data.get("@odata.count")
        row["GroupCount"] = str(count) if count is not None else ""
    else:
        notes.append(f"groups_err_{status}")

    # Notes
    auto_notes = []
    if mailbox_type == "Shared" and row["Licenses"]:
        auto_notes.append("shared_mailbox_still_licensed")
    if row["AccountEnabled"] == "false" and row["Licenses"]:
        auto_notes.append("disabled_account_still_licensed")
    if row["AccountEnabled"] == "false":
        auto_notes.append("account_disabled")
    if row["MFARegistered"] == "false":
        auto_notes.append("no_mfa_registered")
    if row["MFAMethods"] and "Authenticator" not in row["MFAMethods"] and "FIDO2" not in row["MFAMethods"]:
        # Flag weak/SMS-only setups
        methods_set = set(row["MFAMethods"].split(";"))
        non_pw_methods = methods_set - {"Password"}
        if non_pw_methods and non_pw_methods.issubset({"Phone", "Email", "OATH"}):
            if "Phone" in non_pw_methods and len(non_pw_methods) == 1:
                auto_notes.append("sms_only_mfa")
    if row["AdminRoles"] and row["AdminRoles"] != "scope_unavailable":
        auto_notes.append("has_admin_role")

    if auto_notes:
        notes.extend(auto_notes)
    row["Notes"] = ";".join(notes)
    return row


def main():
    token = get_token()
    print(f"Token length: {len(token)}", file=sys.stderr)

    fieldnames = [
        "UPN", "DisplayName", "AccountEnabled", "MailboxType", "Licenses",
        "MFARegistered", "MFAMethods", "DefaultMFAMethod", "LastSignIn",
        "LastInteractiveSignIn", "AdminRoles", "JobTitle", "Department",
        "GroupCount", "Notes",
    ]

    rows = []
    for upn, mbox in USERS:
        print(f"Processing {upn}...", file=sys.stderr)
        row = process_user(token, upn, mbox)
        rows.append(row)
        print(f"  -> {row}", file=sys.stderr)
        time.sleep(1.0)  # slightly more polite; 4 batches running in parallel

    with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=fieldnames, quoting=csv.QUOTE_MINIMAL)
        writer.writeheader()
        for r in rows:
            writer.writerow(r)

    print(f"\nWrote {len(rows)} rows to {OUTPUT_CSV}")


if __name__ == "__main__":
    main()
