#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["requests"]
# ///

import argparse
import sys
import uuid
from datetime import datetime, timezone
from xml.etree import ElementTree

import requests

BASE_URL = "https://cc.connorrhodes.com"
USERNAME = "connor"
PASSWORD = "xK9mN2pQ7vR8wL4jT6hY3sF5dG0nZ1bC"


def get_session():
    s = requests.Session()
    s.auth = (USERNAME, PASSWORD)
    return s


def discover_addressbook(session):
    principal_url = f"{BASE_URL}/{USERNAME}/"
    headers = {"Depth": "1", "Content-Type": "application/xml"}
    body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav"><d:prop><d:resourcetype/><d:displayname/></d:prop></d:propfind>'
    resp = session.request("PROPFIND", principal_url, headers=headers, data=body)
    tree = ElementTree.fromstring(resp.text)
    ns = {"d": "DAV:", "card": "urn:ietf:params:xml:ns:carddav"}
    for resp_elem in tree.findall("d:response", ns):
        href = resp_elem.find("d:href", ns).text
        propstat = resp_elem.find("d:propstat", ns)
        if propstat is not None:
            prop = propstat.find("d:prop", ns)
            if prop is not None:
                rt = prop.find("d:resourcetype", ns)
                if rt is not None and rt.find("card:addressbook", ns) is not None:
                    return href
    return None


def fetch_all_contacts(session, addressbook_href):
    url = f"{BASE_URL}{addressbook_href}"
    headers = {"Depth": "1", "Content-Type": "application/xml"}
    body = '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:getetag/></d:prop></d:propfind>'
    resp = session.request("PROPFIND", url, headers=headers, data=body)
    tree = ElementTree.fromstring(resp.text)
    ns = {"d": "DAV:"}
    contacts = []
    for resp_elem in tree.findall("d:response", ns):
        href = resp_elem.find("d:href", ns).text
        if href.endswith(".vcf"):
            vcf_resp = session.get(f"{BASE_URL}{href}")
            contact = parse_vcard(vcf_resp.text)
            contact["_href"] = href
            contacts.append(contact)
    return contacts


def parse_vcard(text):
    contact = {"fn": "", "phone": "", "email": "", "note": "", "uid": ""}
    unfolded = []
    for line in text.splitlines():
        if line.startswith(" ") or line.startswith("\t"):
            if unfolded:
                unfolded[-1] += line[1:]
        else:
            unfolded.append(line)
    for line in unfolded:
        if line.startswith("FN:"):
            contact["fn"] = line[3:]
        elif line.startswith("TEL"):
            if ":" in line:
                contact["phone"] = line.split(":", 1)[1]
        elif line.startswith("EMAIL"):
            if ":" in line:
                contact["email"] = line.split(":", 1)[1]
        elif line.startswith("NOTE:"):
            contact["note"] = line[5:]
        elif line.startswith("UID:"):
            contact["uid"] = line[4:]
    return contact


def build_vcard(fn, phone="", email="", note="", uid=None):
    if not uid:
        uid = str(uuid.uuid4())
    parts = fn.strip().rsplit(" ", 1)
    last = parts[1] if len(parts) > 1 else ""
    first = parts[0]
    lines = [
        "BEGIN:VCARD",
        "VERSION:3.0",
        f"UID:{uid}",
        f"FN:{fn}",
        f"N:{last};{first};;;",
    ]
    if phone:
        lines.append(f"TEL;TYPE=cell:{phone}")
    if email:
        lines.append(f"EMAIL:{email}")
    if note:
        lines.append(f"NOTE:{note}")
    lines.append(f"REV:{datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}")
    lines.append("END:VCARD")
    return "\r\n".join(lines) + "\r\n"


def print_contact(contact):
    print(f"  Name:  {contact['fn']}")
    if contact["phone"]:
        print(f"  Phone: {contact['phone']}")
    if contact["email"]:
        print(f"  Email: {contact['email']}")
    if contact["note"]:
        print(f"  Note:  {contact['note']}")
    print()


def find_duplicates(contacts, name=None, phone=None, email=None):
    dupes = []
    for c in contacts:
        if phone and c["phone"] == phone:
            dupes.append(c)
            continue
        if email and c["email"].lower() == email.lower():
            dupes.append(c)
            continue
        if name and not phone and not email and c["fn"].lower() == name.lower():
            dupes.append(c)
    return dupes


def search_contacts(contacts, name=None, phone=None, email=None):
    results = []
    for c in contacts:
        match = False
        if name and name.lower() in c["fn"].lower():
            match = True
        if phone and phone in c["phone"]:
            match = True
        if email and email.lower() in c["email"].lower():
            match = True
        if match:
            results.append(c)
    return results


def cmd_create(args, session, addressbook_href):
    contacts = fetch_all_contacts(session, addressbook_href)
    dupes = find_duplicates(contacts, name=args.name, phone=args.phone, email=args.email)
    if dupes:
        print("Contact already exists:", file=sys.stderr)
        for d in dupes:
            print_contact(d)
        sys.exit(1)

    uid = str(uuid.uuid4())
    vcard = build_vcard(args.name, phone=args.phone or "", email=args.email or "", note=args.note or "", uid=uid)
    url = f"{BASE_URL}{addressbook_href}{uid}.vcf"
    resp = session.put(url, data=vcard.encode("utf-8"), headers={"Content-Type": "text/vcard"})
    if resp.ok:
        print(f"Created contact: {args.name}")
    else:
        print(f"Error: {resp.status_code} {resp.text}", file=sys.stderr)
        sys.exit(1)


def cmd_read(args, session, addressbook_href):
    contacts = fetch_all_contacts(session, addressbook_href)
    if args.name or args.phone or args.email:
        contacts = search_contacts(contacts, name=args.name, phone=args.phone, email=args.email)
    if not contacts:
        print("No contacts found.")
        return
    for c in contacts:
        print_contact(c)


def cmd_update(args, session, addressbook_href):
    contacts = fetch_all_contacts(session, addressbook_href)
    matches = search_contacts(contacts, name=args.find, phone=args.find, email=args.find)
    if not matches:
        print(f"No contact found matching '{args.find}'.", file=sys.stderr)
        sys.exit(1)
    if len(matches) > 1:
        print(f"Multiple contacts match '{args.find}', be more specific:", file=sys.stderr)
        for m in matches:
            print_contact(m)
        sys.exit(1)

    contact = matches[0]
    new_fn = args.name if args.name else contact["fn"]
    new_phone = args.phone if args.phone else contact["phone"]
    new_email = args.email if args.email else contact["email"]
    new_note = args.note if args.note else contact["note"]

    vcard = build_vcard(new_fn, phone=new_phone, email=new_email, note=new_note, uid=contact["uid"])
    url = f"{BASE_URL}{contact['_href']}"
    resp = session.put(url, data=vcard.encode("utf-8"), headers={"Content-Type": "text/vcard"})
    if resp.ok:
        print(f"Updated contact: {new_fn}")
    else:
        print(f"Error: {resp.status_code} {resp.text}", file=sys.stderr)
        sys.exit(1)


def cmd_delete(args, session, addressbook_href):
    contacts = fetch_all_contacts(session, addressbook_href)
    matches = search_contacts(contacts, name=args.find, phone=args.find, email=args.find)
    if not matches:
        print(f"No contact found matching '{args.find}'.", file=sys.stderr)
        sys.exit(1)
    if len(matches) > 1:
        print(f"Multiple contacts match '{args.find}', be more specific:", file=sys.stderr)
        for m in matches:
            print_contact(m)
        sys.exit(1)

    contact = matches[0]
    if not args.yes:
        print("About to delete:")
        print_contact(contact)
        answer = input("Confirm? [y/N] ").strip().lower()
        if answer not in ("y", "yes"):
            print("Cancelled.")
            return

    url = f"{BASE_URL}{contact['_href']}"
    resp = session.delete(url)
    if resp.ok:
        print(f"Deleted contact: {contact['fn']}")
    else:
        print(f"Error: {resp.status_code} {resp.text}", file=sys.stderr)
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(description="Manage Radicale contacts")
    sub = parser.add_subparsers(dest="command")

    p_create = sub.add_parser("create", help="Create a new contact")
    p_create.add_argument("--name", required=True, help="Contact name (required)")
    p_create.add_argument("--phone", help="Phone number")
    p_create.add_argument("--email", help="Email address")
    p_create.add_argument("--note", help="Note")

    p_read = sub.add_parser("read", help="Read contacts")
    p_read.add_argument("--name", help="Filter by name")
    p_read.add_argument("--phone", help="Filter by phone")
    p_read.add_argument("--email", help="Filter by email")

    p_update = sub.add_parser("update", help="Update a contact")
    p_update.add_argument("--find", required=True, help="Search term to identify contact")
    p_update.add_argument("--name", help="New name")
    p_update.add_argument("--phone", help="New phone number")
    p_update.add_argument("--email", help="New email address")
    p_update.add_argument("--note", help="New note")

    p_delete = sub.add_parser("delete", help="Delete a contact")
    p_delete.add_argument("--find", required=True, help="Search term to identify contact")
    p_delete.add_argument("-y", "--yes", action="store_true", help="Skip confirmation")

    args = parser.parse_args()
    if not args.command:
        parser.print_help()
        sys.exit(1)

    if args.command == "create" and not any([args.phone, args.email, args.note]):
        print("Error: at least one of --phone, --email, or --note is required", file=sys.stderr)
        sys.exit(1)

    if args.command == "update" and not any([args.name, args.phone, args.email, args.note]):
        print("Error: provide at least one field to update (--name, --phone, --email, --note)", file=sys.stderr)
        sys.exit(1)

    session = get_session()
    addressbook_href = discover_addressbook(session)
    if not addressbook_href:
        print("Error: could not discover addressbook", file=sys.stderr)
        sys.exit(1)

    {"create": cmd_create, "read": cmd_read, "update": cmd_update, "delete": cmd_delete}[args.command](args, session, addressbook_href)


if __name__ == "__main__":
    main()
