"""ASCII plist serializer for .pbxproj files.

Produces the exact format Xcode generates: section-grouped objects,
inline comments on references, proper quoting rules.
"""

import re
from collections import defaultdict
from collections.abc import Iterable
from typing import Any

from .objects import PBXObject

# Types that are serialized on a single line
SINGLE_LINE_TYPES = {"PBXBuildFile", "PBXFileReference"}

# Regex for values that DON'T need quoting: simple identifiers
_UNQUOTED_RE = re.compile(r"^[A-Za-z0-9_.]+$")


def needs_quoting(value: str) -> bool:
    """Check if a string value needs double-quote wrapping in ASCII plist."""
    if not value:
        return True
    return _UNQUOTED_RE.match(value) is None


def quote(value: str) -> str:
    """Quote a value if needed for ASCII plist format."""
    if needs_quoting(value):
        escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
        return f'"{escaped}"'
    return value


def comment_for(obj_id: str, objects: dict[str, PBXObject]) -> str:
    """Get the inline comment string for an object reference."""
    obj = objects.get(obj_id)
    if obj and obj.comment:
        return f" /* {obj.comment} */"
    return ""


def _serialize_value(
    value: Any,
    objects: dict[str, PBXObject],
    indent: int,
    single_line: bool = False,
) -> str:
    """Recursively serialize a value to ASCII plist format."""
    tabs = "\t" * indent

    if isinstance(value, dict):
        if not value:
            return "{\n" + tabs + "}"
        if single_line:
            parts = []
            for k in sorted(value.keys()):
                v = _serialize_value(value[k], objects, indent, single_line=True)
                parts.append(f"{k} = {v};")
            return "{" + " ".join(parts) + " }"

        lines = ["{\n"]
        for k in sorted(value.keys()):
            v = value[k]
            serialized = _serialize_value(v, objects, indent + 1)
            lines.append(f"{tabs}\t{k} = {serialized};\n")
        lines.append(f"{tabs}}}")
        return "".join(lines)

    if isinstance(value, (list, tuple)):
        if not value:
            if single_line:
                return "( )"
            return "(\n" + tabs + ")"
        if single_line:
            parts = []
            for item in value:
                s = _serialize_value(item, objects, indent, single_line=True)
                # Add comment for object references in lists
                if isinstance(item, str) and item in objects:
                    s += comment_for(item, objects)
                parts.append(s)
            return "(" + ", ".join(parts) + ", )"

        lines = ["(\n"]
        for item in value:
            s = _serialize_value(item, objects, indent + 1)
            if isinstance(item, str) and item in objects:
                s += comment_for(item, objects)
            lines.append(f"{tabs}\t{s},\n")
        lines.append(f"{tabs})")
        return "".join(lines)

    # Scalar string
    return quote(str(value))


def _isa_first(keys: Iterable[str]) -> list[str]:
    """Sort keys alphabetically but always put 'isa' first (matching Xcode output)."""
    key_set = set(keys)
    rest = sorted(k for k in key_set if k != "isa")
    if "isa" in key_set:
        return ["isa", *rest]
    return rest


def serialize_object(
    obj_id: str,
    obj: PBXObject,
    objects: dict[str, PBXObject],
) -> str:
    """Serialize a single PBX object entry."""
    d = obj.to_dict()
    cmt = comment_for(obj_id, objects)
    single_line = obj.isa in SINGLE_LINE_TYPES

    if single_line:
        parts = []
        for k in _isa_first(d.keys()):
            v = d[k]
            serialized = _serialize_value(v, objects, 2, single_line=True)
            # Add comment for object ID references
            if isinstance(v, str) and v in objects:
                serialized += comment_for(v, objects)
            parts.append(f"{k} = {serialized};")
        return f"\t\t{obj_id}{cmt} = {{{' '.join(parts)} }};\n"

    lines = [f"\t\t{obj_id}{cmt} = {{\n"]
    for k in _isa_first(d.keys()):
        v = d[k]
        serialized = _serialize_value(v, objects, 3)
        # Add comment for object ID references
        if isinstance(v, str) and v in objects:
            serialized += comment_for(v, objects)
        lines.append(f"\t\t\t{k} = {serialized};\n")
    lines.append("\t\t};\n")
    return "".join(lines)


def serialize_project(
    root_object_id: str,
    objects: dict[str, PBXObject],
    *,
    object_version: str = "56",
) -> str:
    """Serialize a complete .pbxproj file.

    Args:
        root_object_id: The object key of the PBXProject root object.
        objects: Dict mapping object keys to PBXObject instances.
        object_version: The pbxproj objectVersion (default "56").

    Returns:
        The complete ASCII plist file content as a string.

    """
    # Group objects by isa
    sections: dict[str, list[tuple[str, PBXObject]]] = defaultdict(list)
    for obj_id, obj in objects.items():
        sections[obj.isa].append((obj_id, obj))

    # Sort sections alphabetically, objects within each section by key
    lines = [
        "// !$*UTF8*$!\n",
        "{\n",
        "\tarchiveVersion = 1;\n",
        "\tclasses = {\n",
        "\t};\n",
        f"\tobjectVersion = {object_version};\n",
        "\tobjects = {\n",
    ]

    for section_name in sorted(sections.keys()):
        entries = sorted(sections[section_name], key=lambda x: x[0])
        lines.append(f"\n/* Begin {section_name} section */\n")
        for obj_id, obj in entries:
            lines.append(serialize_object(obj_id, obj, objects))
        lines.append(f"/* End {section_name} section */\n")

    root_cmt = comment_for(root_object_id, objects)
    lines.append("\t};\n")
    lines.append(f"\trootObject = {root_object_id}{root_cmt};\n")
    lines.append("}\n")

    return "".join(lines)
