"""Design system + token REST endpoints (milestone #254 step 4).

Wraps `services/design_systems.py`, which owns the ACL and the cycle guard.
Design systems are owner-scoped top-level records rather than project-scoped
ones, so these do NOT nest under `/api/projects/` — `routes/rulebooks.py` is the
closer shape.

Two failures this layer has to keep apart, which is why the service raises for
one and returns None for the other:

  - `DesignSystemCycle` -> 400 with its message. "That parent already inherits
    from this system" is a correctable mistake and the caller needs to be told
    which one they made.
  - None -> 404, covering both "no such system" and "not yours". Conflating
    those two IS the intent: distinguishing them would confirm the existence of
    records the caller may not see.
"""
from quart import Blueprint, jsonify, request

from scribe.auth import get_current_user_id, login_required
from scribe.routes.utils import not_found
from scribe.services import design_systems as ds_svc
from scribe.services.design_starter_roles import (
    DEFAULT_TOKEN_PREFIX,
    describe_groups,
)
from scribe.services.design_systems import DesignSystemCycle

design_systems_bp = Blueprint("design_systems", __name__, url_prefix="/api")




# ── Design systems ──────────────────────────────────────────────────────

@design_systems_bp.get("/design-systems")
@login_required
async def list_design_systems():
    """The caller's design systems. An empty list is the ordinary state for an
    install that has never made one, not an error."""
    rows = await ds_svc.list_design_systems(get_current_user_id())
    return jsonify({"design_systems": [s.to_dict() for s in rows]})


@design_systems_bp.post("/design-systems")
@login_required
async def create_design_system():
    data = await request.get_json() or {}
    title = (data.get("title") or "").strip()
    if not title:
        return jsonify({"error": "title is required"}), 400
    system = await ds_svc.create_design_system(
        user_id=get_current_user_id(),
        title=title,
        description=data.get("description") or None,
        guidance=data.get("guidance") or None,
        parent_id=data.get("parent_id"),
        starter_role_groups=data.get("starter_role_groups"),
        token_prefix=data.get("token_prefix") or DEFAULT_TOKEN_PREFIX,
    )
    if system is None:
        return jsonify({"error": "parent design system not found"}), 404
    return jsonify(system.to_dict()), 201


@design_systems_bp.get("/design-systems/starter-roles")
@login_required
async def list_starter_role_groups():
    """The starter role catalogue, for the creation form's checklist.

    Roles and purposes only — no values, ever. See services/design_starter_roles.
    """
    return jsonify({
        "groups": describe_groups(),
        "default_prefix": DEFAULT_TOKEN_PREFIX,
    })


@design_systems_bp.get("/design-systems/<int:design_system_id>")
@login_required
async def get_design_system(design_system_id: int):
    system = await ds_svc.get_design_system(get_current_user_id(), design_system_id)
    if system is None:
        return not_found("Design system")
    return jsonify(system.to_dict())


@design_systems_bp.patch("/design-systems/<int:design_system_id>")
@login_required
async def update_design_system(design_system_id: int):
    data = await request.get_json() or {}
    fields = {
        k: v for k, v in data.items() if k in ("title", "description", "guidance")
    }
    # Presence, not truthiness: `{"parent_id": null}` means "make this a root",
    # which a `if data.get("parent_id")` filter would silently drop.
    if "parent_id" in data:
        fields["parent_id"] = data["parent_id"]
    try:
        system = await ds_svc.update_design_system(get_current_user_id(), design_system_id, **fields)
    except DesignSystemCycle as exc:
        return jsonify({"error": str(exc)}), 400
    if system is None:
        return not_found("Design system")
    return jsonify(system.to_dict())


@design_systems_bp.delete("/design-systems/<int:design_system_id>")
@login_required
async def delete_design_system(design_system_id: int):
    if not await ds_svc.delete_design_system(get_current_user_id(), design_system_id):
        return not_found("Design system")
    return "", 204


@design_systems_bp.get("/design-systems/<int:design_system_id>/resolved")
@login_required
async def resolve_design_system(design_system_id: int):
    """The EFFECTIVE token set — everything inherited, with this system's on top.

    Distinct from `/tokens` on purpose: that returns what this system CHANGES,
    this returns what it ends up being. Both are real questions and answering
    only one would make the other a client-side computation.
    """
    resolved = await ds_svc.resolve_design_system(get_current_user_id(), design_system_id)
    if resolved is None:
        return not_found("Design system")
    return jsonify({
        "design_system_id": design_system_id,
        "tokens": [t.to_dict() for t in resolved],
    })


@design_systems_bp.get("/design-systems/<int:design_system_id>/stylesheet")
@login_required
async def get_design_system_stylesheet(design_system_id: int):
    """The master CSS sheet this design system generates.

    JSON by default (the UI wants the reuse report alongside the CSS); add
    `?format=css` for the raw stylesheet as `text/css`, which is what a build
    step or a `curl` wants.

    `?root=` overrides the base selector — a container-scoped preview cannot use
    `:root`, so the generator takes it as a parameter.
    """
    root = (request.args.get("root") or ":root").strip() or ":root"
    result = await ds_svc.stylesheet_for_system(get_current_user_id(), design_system_id, root)
    if result is None:
        return not_found("Design system")
    if request.args.get("format") == "css":
        return result["css"], 200, {"Content-Type": "text/css; charset=utf-8"}
    return jsonify(result)


@design_systems_bp.get("/design-systems/<int:design_system_id>/snippet-check")
@login_required
async def check_snippets_against_system(design_system_id: int):
    """Which recorded snippets disagree with this system's sheet.

    `?project_id=` narrows to one project; omit it to check every project, which
    is usually right — a component recorded elsewhere still has to use the same
    tags.
    """
    project_id = request.args.get("project_id", type=int) or 0
    result = await ds_svc.check_snippets_against_system(
        get_current_user_id(), design_system_id, project_id
    )
    if result is None:
        return not_found("Design system")
    return jsonify(result)


# ── Tokens ──────────────────────────────────────────────────────────────

@design_systems_bp.get("/design-systems/<int:design_system_id>/tokens")
@login_required
async def list_design_tokens(design_system_id: int):
    """This system's OWN tokens — its override set, not its effective set."""
    if await ds_svc.get_design_system(get_current_user_id(), design_system_id) is None:
        return not_found("Design system")
    rows = await ds_svc.list_tokens(get_current_user_id(), design_system_id)
    return jsonify({"tokens": [t.to_dict() for t in rows]})


@design_systems_bp.post("/design-systems/<int:design_system_id>/tokens")
@login_required
async def create_design_token(design_system_id: int):
    data = await request.get_json() or {}
    name = (data.get("name") or "").strip()
    if not name:
        return jsonify({"error": "name is required"}), 400
    token = await ds_svc.create_token(
        user_id=get_current_user_id(),
        design_system_id=design_system_id,
        name=name,
        value_by_mode=data.get("value_by_mode"),
        group_name=data.get("group_name") or None,
        purpose=data.get("purpose") or None,
        rationale=data.get("rationale") or None,
        supersedes=data.get("supersedes"),
        order_index=data.get("order_index") or 0,
    )
    if token is None:
        return not_found("Design system")
    return jsonify(token.to_dict()), 201


@design_systems_bp.patch("/design-tokens/<int:token_id>")
@login_required
async def update_design_token(token_id: int):
    data = await request.get_json() or {}
    fields = {
        k: v for k, v in data.items()
        if k in (
            "name", "value_by_mode", "group_name", "purpose", "rationale",
            "supersedes", "order_index",
        )
    }
    token = await ds_svc.update_token(get_current_user_id(), token_id, **fields)
    if token is None:
        return not_found("Design token")
    return jsonify(token.to_dict())


@design_systems_bp.delete("/design-tokens/<int:token_id>")
@login_required
async def delete_design_token(token_id: int):
    if not await ds_svc.delete_token(get_current_user_id(), token_id):
        return not_found("Design token")
    return "", 204


# ── The project pointer ─────────────────────────────────────────────────

@design_systems_bp.put("/projects/<int:project_id>/design-system")
@login_required
async def set_project_design_system(project_id: int):
    """Point a project at a design system. `{"design_system_id": null}` clears it.

    PUT rather than PATCH: this sets one field to exactly what is sent, and
    clearing it is a first-class outcome rather than an omission.
    """
    data = await request.get_json() or {}
    ok = await ds_svc.set_project_design_system(
        get_current_user_id(), project_id, data.get("design_system_id")
    )
    if not ok:
        return not_found("Project or design system")
    return jsonify({"project_id": project_id,
                    "design_system_id": data.get("design_system_id")})
