import re
from dataclasses import dataclass
from typing import Any


@dataclass(frozen=True)
class PlantUmlOptions:
    direction: str = "LR"  # LR/TB


_IDENTIFIER_RE = re.compile(r"^[A-Za-z_]\w*$")


def _safe_id(name: str) -> str:
    s = re.sub(r"[^0-9a-zA-Z_]", "_", name)
    if re.match(r"^\d", s):
        s = "_" + s
    return s


def _unique_ids(names: list[str]) -> dict[str, str]:
    used: set[str] = set()
    out: dict[str, str] = {}
    for name in names:
        base = _safe_id(name)
        candidate = base
        i = 2
        while candidate in used:
            candidate = f"{base}_{i}"
            i += 1
        used.add(candidate)
        out[name] = candidate
    return out


def _need_quote_member(member: str) -> bool:
    return _IDENTIFIER_RE.match(member) is None


def _member_ref(class_id: str, member: str) -> str:
    if _need_quote_member(member):
        escaped = member.replace('"', '\\"')
        return f'{class_id}::"{escaped}"'
    return f"{class_id}::{member}"


def _ref_to_schema_name(ref: str) -> str:
    m = re.search(r"#/components/schemas/([^/]+)$", ref)
    if m:
        return m.group(1)
    return ref.split("/")[-1].split("#")[0]


def _collect_properties(schema: dict[str, Any]) -> dict[str, Any]:
    props: dict[str, Any] = {}
    direct = schema.get("properties")
    if isinstance(direct, dict):
        props.update(direct)

    all_of = schema.get("allOf")
    if isinstance(all_of, list):
        for part in all_of:
            if isinstance(part, dict):
                part_props = part.get("properties")
                if isinstance(part_props, dict):
                    props.update(part_props)
    return props


def _prop_type_str(prop: dict[str, Any]) -> tuple[str, str | None, bool]:
    if "$ref" in prop:
        target = _ref_to_schema_name(str(prop["$ref"]))
        return (target, target, False)

    if prop.get("type") == "array":
        items = prop.get("items") or {}
        if isinstance(items, dict) and "$ref" in items:
            target = _ref_to_schema_name(str(items["$ref"]))
            return (f"{target}[]", target, True)
        return ("array", None, False)

    if "enum" in prop and isinstance(prop.get("enum"), list):
        return ("enum", None, False)

    return (str(prop.get("type") or "object"), None, False)


def _pick_anchor_field(target_schema: dict[str, Any]) -> str | None:
    props = _collect_properties(target_schema)
    if not props:
        return None
    if "id" in props:
        return "id"
    return next(iter(props.keys()))


def openapi_schemas_to_plantuml(
    *,
    spec: dict[str, Any],
    options: PlantUmlOptions | None = None,
) -> str:
    options = options or PlantUmlOptions()
    schemas = (((spec.get("components") or {}).get("schemas")) or {})
    if not isinstance(schemas, dict) or not schemas:
        raise ValueError("未找到 components/schemas。")

    schema_names = [str(k) for k in schemas.keys()]
    class_id = _unique_ids(schema_names)

    lines: list[str] = []
    lines.append("@startuml")
    if options.direction == "LR":
        lines.append("left to right direction")
    else:
        lines.append("top to bottom direction")
    lines.append("skinparam classAttributeIconSize 0")

    # 1) Class definitions
    for name in schema_names:
        schema = schemas.get(name)
        if not isinstance(schema, dict):
            continue

        cid = class_id[name]
        lines.append(f'class "{name}" as {cid} {{')

        props = _collect_properties(schema)
        if props:
            for prop_name, prop_def in props.items():
                if not isinstance(prop_def, dict):
                    lines.append(f"  + {prop_name}")
                    continue
                type_str, _, _ = _prop_type_str(prop_def)
                lines.append(f"  + {prop_name} : {type_str}")
        else:
            lines.append("  ..")

        lines.append("}")

    # 2) Inheritance via allOf $ref
    for name in schema_names:
        schema = schemas.get(name)
        if not isinstance(schema, dict):
            continue
        all_of = schema.get("allOf")
        if not isinstance(all_of, list):
            continue
        for part in all_of:
            if isinstance(part, dict) and "$ref" in part:
                parent = _ref_to_schema_name(str(part["$ref"]))
                if parent in class_id:
                    lines.append(f"{class_id[parent]} <|-- {class_id[name]}")

    # 3) Member-level links for $ref properties
    for name in schema_names:
        schema = schemas.get(name)
        if not isinstance(schema, dict):
            continue
        props = _collect_properties(schema)
        if not props:
            continue

        for prop_name, prop_def in props.items():
            if not isinstance(prop_def, dict):
                continue
            _, target, is_array_ref = _prop_type_str(prop_def)
            if not target or target not in schemas:
                continue

            src = _member_ref(class_id[name], str(prop_name))
            target_schema = schemas.get(target)
            if not isinstance(target_schema, dict):
                dst = class_id[target]
            else:
                anchor = _pick_anchor_field(target_schema)
                dst = _member_ref(class_id[target], anchor) if anchor else class_id[target]

            label = str(prop_name) + ("[*]" if is_array_ref else "")
            lines.append(f"{src} --> {dst} : {label}")

    lines.append("@enduml")
    return "\n".join(lines) + "\n"
