#!/usr/bin/env python3
"""Batch 6 - Cascades Tucson per-user M365 detail for HIPAA planning."""
import csv
import json
import time
import urllib.request
import urllib.parse
import urllib.error

with open(r"C:\claudetools\clients\cascades-tucson\reports\user-detail-batches\.token.txt", "r") as f:
    TOKEN = f.read().strip()

USERS = [
    "ramon.castaneda@cascadestucson.com",
    "security@cascadestucson.com",
    "sharon.edwards@cascadestucson.com",
    "susan.hicks@cascadestucson.com",
    "tamra.matthews@cascadestucson.com",
]

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

HEADERS_BASE = {"Authorization": f"Bearer {TOKEN}", "Accept": "application/json"}


def graph_get(url, extra_headers=None, max_retries=4):
    headers = dict(HEADERS_BASE)
    if extra_headers:
        headers.update(extra_headers)
    for attempt in range(max_retries):
        req = urllib.request.Request(url, headers=headers)
        try:
            with urllib.request.urlopen(req, timeout=30) as resp:
                body = resp.read().decode("utf-8")
                return json.loads(body) if body else {}
        except urllib.error.HTTPError as e:
            body = e.read().decode("utf-8", errors="replace")
            if e.code == 429:
                wait = int(e.headers.get("Retry-After", "5"))
                time.sleep(wait)
                continue
            if e.code in (500, 502, 503, 504):
                time.sleep(2 ** attempt)
                continue
            return {"_error": f"HTTP {e.code}", "_body": body[:500]}
        except Exception as e:
            if attempt == max_retries - 1:
                return {"_error": str(e)[:200]}
            time.sleep(2 ** attempt)
    return {"_error": "max retries"}


# SKU map - common ones
SKU_MAP = {
    "05e9a617-0261-4cee-bb44-138d3ef5d965": "M365 E3",
    "06ebc4ee-1bb5-47dd-8120-11324bc54e06": "M365 E5",
    "c42b9cae-ea4f-4ab7-9717-81576235ccac": "M365 Dev E5",
    "f245ecc8-75af-4f8e-b61f-27d8114de5f3": "M365 Business Standard",
    "cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46": "M365 Business Premium",
    "3b555118-da6a-4418-894f-7df1e2096870": "M365 Business Basic",
    "4b9405b0-7788-4568-add1-99614e613b69": "Exchange Online (Plan 1)",
    "19ec0d23-8335-4cbd-94ac-6050e30712fa": "Exchange Online (Plan 2)",
    "c5928f49-12ba-48f7-ada3-0d743a3601d5": "Visio Plan 2",
    "f30db892-07e9-47e9-837c-80727f46fd3d": "Power Automate Free",
    "f8a1db68-be16-40ed-86d5-cb42ce701560": "Power BI Pro",
    "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235": "Power BI (free)",
    "1f2f344a-700d-42c9-9427-5cea1d5d7ba6": "Stream",
    "6470687e-a428-4b7a-bef2-8a291cdf4c05": "Windows Store for Business",
    "6fd2c87f-b296-42f0-b197-1e91e994b900": "Office 365 E3",
    "c7df2760-2c81-4ef7-b578-5b5392b571df": "Office 365 E5",
    "18181a46-0d4e-45cd-891e-60aabd171b4e": "Office 365 E1",
    "bd09678e-b83c-4d3f-aaba-3dad4abd128b": "Teams EEA",
    "52ea0e27-ae73-4983-a08f-13561ebdb823": "Teams Exploratory Dept",
    "710779e8-3d4a-4c88-adb9-386c958d1fdf": "Teams Exploratory",
    "3f4babde-90ec-47c6-995d-d223749065d1": "Teams Commercial Cloud",
    "66b55226-6b4f-492c-910c-a3b7a3c9d993": "M365 F3",
    "44575a31-1921-4b6b-b11c-ca05f2450d6f": "M365 F1",
    "dcb1a3ae-b33f-4487-846a-a640262fadf4": "M365 Apps for Business",
    "c2273bd0-dff7-4215-9ef5-2c7bcfb06425": "M365 Apps for Enterprise",
    "4b585984-651b-448a-9e53-3b10f069cf7f": "F3 Defender",
    "26d45bd9-adf1-46cd-a9e1-51e9a5524128": "EOP (Enterprise Mobility E3)",
    "b05e124f-c7cc-45a0-a6aa-8cf78c946968": "EMS E5",
    "84a661c4-e949-4bd2-a560-ed7766fcaf2b": "AAD Premium P2",
    "078d2b04-f1bd-4111-bbd4-b4b1b354cef4": "AAD Premium P1",
    "efccb6f7-5641-4e0e-bd10-b4976e1bf68e": "EMS E3",
    "53818b1b-4a27-454b-8896-0dba576410e6": "Project Plan 3",
    "09015f9f-377f-4538-bbb5-f75ceb09358a": "Project Plan 5",
    "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235": "Power BI Free",
    "e43b5b99-8dfb-405f-9987-dc307f34bcbd": "Phone System",
    "3ab6abff-666f-4424-bfb7-f0bc274ec7bc": "Teams Essentials",
    "dcf0408c-aaec-446c-afd4-43e3683943ea": "Teams Premium",
}


def sku_name(sku_id, sku_part):
    if sku_id in SKU_MAP:
        return SKU_MAP[sku_id]
    return sku_part or sku_id[:8]


def csv_quote(s):
    s = "" if s is None else str(s)
    if "," in s or '"' in s or "\n" in s:
        return '"' + s.replace('"', '""') + '"'
    return s


rows = []
failures = []
stats = {"no_mfa": [], "sms_only": [], "stale": [], "admin": []}

for i, upn in enumerate(USERS):
    print(f"[{i+1}/{len(USERS)}] {upn}")
    notes = []
    upn_enc = urllib.parse.quote(upn, safe="@")

    # 1a. Profile
    prof = graph_get(f"https://graph.microsoft.com/v1.0/users/{upn_enc}?$select=id,userPrincipalName,displayName,accountEnabled,jobTitle,department")
    if "_error" in prof:
        failures.append(f"{upn}: profile {prof['_error']}")
        rows.append([upn, "", "", "User", "", "", "", "", "", "", "", "", "", "", f"profile-error:{prof['_error']}"])
        time.sleep(0.5)
        continue

    user_id = prof.get("id", "")
    display = prof.get("displayName", "") or ""
    enabled = prof.get("accountEnabled", "")
    job = prof.get("jobTitle", "") or ""
    dept = prof.get("department", "") or ""

    # 1b. signInActivity (requires GUID, not UPN)
    sia = graph_get(f"https://graph.microsoft.com/v1.0/users/{user_id}?$select=signInActivity")
    last_signin = ""
    last_interactive = ""
    if "_error" not in sia:
        s = sia.get("signInActivity") or {}
        last_signin = s.get("lastSignInDateTime", "") or ""
        last_interactive = s.get("lastNonInteractiveSignInDateTime", "") or s.get("lastSignInDateTime", "") or ""
        # Actually: lastSignIn = lastInteractive, lastNonInteractive = noninteractive
        last_signin = s.get("lastNonInteractiveSignInDateTime", "") or s.get("lastSignInDateTime", "") or ""
        last_interactive = s.get("lastSignInDateTime", "") or ""
    else:
        notes.append(f"signIn-err")

    # Stale detection
    if last_interactive:
        try:
            from datetime import datetime, timezone
            dt = datetime.fromisoformat(last_interactive.replace("Z", "+00:00"))
            days = (datetime.now(timezone.utc) - dt).days
            if days > 90:
                stats["stale"].append(f"{upn} ({days}d)")
        except Exception:
            pass
    elif enabled:
        stats["stale"].append(f"{upn} (never)")

    # 2. Licenses
    lic_resp = graph_get(f"https://graph.microsoft.com/v1.0/users/{upn_enc}/licenseDetails")
    lic_names = []
    if "_error" not in lic_resp:
        for l in lic_resp.get("value", []):
            lic_names.append(sku_name(l.get("skuId", ""), l.get("skuPartNumber", "")))
    else:
        notes.append("lic-err")
    licenses = ";".join(lic_names)

    # 3. MFA methods
    mfa_resp = graph_get(f"https://graph.microsoft.com/v1.0/users/{upn_enc}/authentication/methods")
    mfa_methods = []
    if "_error" not in mfa_resp:
        for m in mfa_resp.get("value", []):
            t = m.get("@odata.type", "")
            short = t.split(".")[-1].replace("AuthenticationMethod", "")
            mfa_methods.append(short)
    else:
        notes.append("mfa-err")

    # Filter out passwordAuth as "MFA"
    non_password = [m for m in mfa_methods if m.lower() not in ("password",)]
    mfa_registered = "Yes" if non_password else "No"
    mfa_methods_str = ";".join(mfa_methods)

    if not non_password and enabled:
        stats["no_mfa"].append(upn)

    # SMS-only detection (only method besides password is phone/sms)
    strong = [m for m in non_password if m.lower() not in ("phone", "sms", "email")]
    if non_password and not strong:
        if any(m.lower() in ("phone", "sms") for m in non_password):
            stats["sms_only"].append(upn)

    # 4. Default MFA
    pref_resp = graph_get(f"https://graph.microsoft.com/beta/users/{upn_enc}/authentication/signInPreferences")
    default_mfa = ""
    if "_error" not in pref_resp:
        default_mfa = pref_resp.get("userPreferredMethodForSecondaryAuthentication", "") or ""
    else:
        notes.append("pref-err")

    # 5. Roles
    roles_resp = graph_get(f"https://graph.microsoft.com/v1.0/users/{upn_enc}/transitiveMemberOf/microsoft.graph.directoryRole")
    roles = []
    if "_error" not in roles_resp:
        for r in roles_resp.get("value", []):
            roles.append(r.get("displayName", ""))
    else:
        notes.append("roles-err")
    admin_roles = ";".join(roles)
    if roles:
        stats["admin"].append(f"{upn} [{admin_roles}]")

    # 6. Group count
    grp_resp = graph_get(
        f"https://graph.microsoft.com/v1.0/users/{upn_enc}/memberOf?$count=true&$top=1",
        extra_headers={"ConsistencyLevel": "eventual"},
    )
    group_count = ""
    if "_error" not in grp_resp:
        group_count = grp_resp.get("@odata.count", "")
    else:
        notes.append("grp-err")

    rows.append([
        upn, display, str(enabled), "User", licenses,
        mfa_registered, mfa_methods_str, default_mfa,
        last_signin, last_interactive, admin_roles,
        job, dept, str(group_count), ";".join(notes)
    ])

    time.sleep(0.5)

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

with open(OUT_CSV, "w", encoding="utf-8", newline="") as f:
    f.write(",".join(header) + "\n")
    for r in rows:
        f.write(",".join(csv_quote(c) for c in r) + "\n")

print(f"\nRows written: {len(rows)}")
print(f"Failures: {len(failures)}")
for fa in failures:
    print(f"  - {fa}")
print(f"\nNo-MFA: {len(stats['no_mfa'])}")
for u in stats["no_mfa"]:
    print(f"  - {u}")
print(f"SMS-only: {len(stats['sms_only'])}")
for u in stats["sms_only"]:
    print(f"  - {u}")
print(f"Stale (>90d or never): {len(stats['stale'])}")
for u in stats["stale"]:
    print(f"  - {u}")
print(f"Admin roles: {len(stats['admin'])}")
for u in stats["admin"]:
    print(f"  - {u}")
