"""vCard 4.0 operations for contact management.

Uses ``vobject`` for parsing and serialization. One .vcf file per contact,
filename derived from FN (full name) as lowercase-hyphenated slug.
"""

from __future__ import annotations

import logging
import re
import unicodedata
from pathlib import Path
from typing import Any

import vobject

logger = logging.getLogger(__name__)


def _slugify(name: str) -> str:
    """Convert a full name to a filesystem-safe slug.

    >>> _slugify("John Doe")
    'john-doe'
    >>> _slugify("María José García")
    'maria-jose-garcia'
    """
    # Normalize unicode → ASCII approximation
    nfkd = unicodedata.normalize("NFKD", name)
    ascii_str = nfkd.encode("ascii", "ignore").decode("ascii")
    # Lowercase, replace non-alphanum with hyphens, collapse multiples
    slug = re.sub(r"[^a-z0-9]+", "-", ascii_str.lower()).strip("-")
    return slug or "unnamed"


def _get_field(vcard: vobject.base.Component, field: str) -> str:
    """Safely extract a single-value field from a vCard component."""
    try:
        val = getattr(vcard, field).value
        # vobject wraps ORG as a list of strings
        if isinstance(val, list):
            return str(val[0]).strip() if val else ""
        return str(val).strip()
    except AttributeError:
        return ""


def _get_list_field(vcard: vobject.base.Component, field: str) -> list[str]:
    """Extract all values for a repeated field (e.g. EMAIL, TEL)."""
    values = []
    for child in vcard.contents.get(field, []):
        val = str(child.value).strip()
        if val:
            values.append(val)
    return values


def parse_vcard(path: Path) -> dict[str, Any]:
    """Parse a .vcf file into a dictionary.

    Returns:
        Dictionary with keys: fn, email, tel, org, title, note, tags,
        x_fields (dict of X-ICARUS-* fields), and _path.
    """
    text = path.read_text(encoding="utf-8")
    vcards = list(vobject.readComponents(text))
    if not vcards:
        return {"fn": path.stem, "tags": [], "x_fields": {}, "_path": path}

    vcard = vcards[0]
    # Collect X-ICARUS-* fields
    x_fields: dict[str, str] = {}
    for key, children in vcard.contents.items():
        if key.upper().startswith("X-ICARUS-"):
            x_fields[key.upper()] = str(children[0].value).strip()

    # First-class tags from X-ICARUS-TAGS (parity with todos)
    tags_raw = x_fields.get("X-ICARUS-TAGS", "")
    tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else []

    return {
        "fn": _get_field(vcard, "fn"),
        "email": _get_list_field(vcard, "email"),
        "tel": _get_list_field(vcard, "tel"),
        "org": _get_field(vcard, "org"),
        "title": _get_field(vcard, "title"),
        "note": _get_field(vcard, "note"),
        "tags": tags,
        "x_fields": x_fields,
        "_path": path,
    }


def write_vcard(path: Path, data: dict[str, Any]) -> None:
    """Write a contact dictionary to a .vcf file (vCard 4.0)."""
    vcard = vobject.vCard()
    vcard.add("version").value = "4.0"

    # FN (required)
    fn = data.get("fn", "")
    vcard.add("fn").value = fn

    # N (structured name) — derive from FN
    parts = fn.split(None, 1) if fn else [""]
    given = parts[0] if parts else ""
    family = parts[1] if len(parts) > 1 else ""
    n = vcard.add("n")
    n.value = vobject.vcard.Name(family=family, given=given)

    # Optional fields
    for email in data.get("email", []):
        vcard.add("email").value = email
    for tel in data.get("tel", []):
        vcard.add("tel").value = tel
    if data.get("org"):
        vcard.add("org").value = [data["org"]]
    if data.get("title"):
        vcard.add("title").value = data["title"]
    if data.get("note"):
        vcard.add("note").value = data["note"]

    # First-class tags → X-ICARUS-TAGS
    tags = data.get("tags", [])
    if tags:
        if isinstance(tags, list):
            tags_str = ", ".join(tags)
        else:
            tags_str = str(tags)
        # Merge into x_fields so it gets written below (avoids duplicate)
        x_fields = dict(data.get("x_fields", {}))
        x_fields["X-ICARUS-TAGS"] = tags_str
    else:
        x_fields = dict(data.get("x_fields", {}))
        if "tags" in data:  # explicitly set (even if empty) — clear the x_field
            x_fields.pop("X-ICARUS-TAGS", None)

    # X-ICARUS-* custom fields
    for key, val in x_fields.items():
        vcard.add(key).value = val

    path.write_text(vcard.serialize(), encoding="utf-8")


def search_contacts(directory: Path, query: str = "") -> list[dict[str, Any]]:
    """Search contacts by fuzzy matching on FN, EMAIL, and ORG.

    If query is empty, returns all contacts.

    Returns:
        List of contact dicts, sorted by relevance (best match first).
    """
    if not directory.exists():
        return []

    results: list[tuple[int, dict[str, Any]]] = []
    query_lower = query.lower()

    for vcf_path in sorted(directory.glob("*.vcf")):
        try:
            contact = parse_vcard(vcf_path)
        except Exception as e:  # noqa: BLE001
            logger.warning("Failed to parse %s: %s", vcf_path, e)
            continue

        if not query:
            results.append((0, contact))
            continue

        # Score: higher = better match
        score = 0
        fn_lower = contact.get("fn", "").lower()
        if query_lower in fn_lower:
            score += 10
        if fn_lower.startswith(query_lower):
            score += 5

        # Check emails
        for email in contact.get("email", []):
            if query_lower in email.lower():
                score += 8

        # Check org
        org = contact.get("org", "").lower()
        if query_lower in org:
            score += 5

        # Check note
        note = contact.get("note", "").lower()
        if query_lower in note:
            score += 2

        if score > 0:
            results.append((score, contact))

    # Sort by score descending
    results.sort(key=lambda x: x[0], reverse=True)
    return [contact for _, contact in results]


def create_contact(directory: Path, data: dict[str, Any]) -> Path:
    """Create a new contact .vcf file.

    Returns:
        Path to the created file.
    """
    directory.mkdir(parents=True, exist_ok=True)
    fn = data.get("fn", "unnamed")
    slug = _slugify(fn)
    path = directory / f"{slug}.vcf"

    # Avoid overwriting — append number if needed
    counter = 1
    while path.exists():
        counter += 1
        path = directory / f"{slug}-{counter}.vcf"

    write_vcard(path, data)
    return path


def update_contact(directory: Path, slug: str, updates: dict[str, Any]) -> Path:
    """Update an existing contact by slug (filename without .vcf).

    Merges updates into the existing contact data.

    Returns:
        Path to the updated file.

    Raises:
        FileNotFoundError: If no contact with the given slug exists.
    """
    path = directory / f"{slug}.vcf"
    if not path.resolve().is_relative_to(directory.resolve()):
        msg = f"Invalid contact slug: {slug}"
        raise ValueError(msg)
    if not path.exists():
        msg = f"Contact not found: {slug}"
        raise FileNotFoundError(msg)

    contact = parse_vcard(path)
    # Merge: lists are replaced, not appended
    for key, value in updates.items():
        if key.startswith("_"):
            continue
        contact[key] = value

    write_vcard(path, contact)
    return path
