#!/usr/bin/env python3
"""Wolai OpenAPI CRUD orchestrator."""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, List, Optional


class WolaiError(Exception):
    def __init__(self, message: str, code: str = "wolai_error", detail: Any = None):
        super().__init__(message)
        self.code = code
        self.detail = detail


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="Wolai note CRUD via OpenAPI")
    parser.add_argument(
        "--op",
        required=True,
        choices=["create", "read", "read-children", "update", "delete", "search"],
        help="wolai operation",
    )
    parser.add_argument("--id", help="block/page id")
    parser.add_argument("--parent-id", help="parent page or block id")
    parser.add_argument("--content", help="plain text content for simple text blocks")
    parser.add_argument(
        "--block-json",
        help='raw JSON for blocks payload, e.g. {"type":"text","content":"Hello"}',
    )
    parser.add_argument("--query", help="keyword for lightweight search")
    parser.add_argument("--cursor", help="pagination cursor")
    parser.add_argument(
        "--limit",
        type=int,
        default=50,
        help="page size for list endpoints (max 200 recommended)",
    )

    parser.add_argument(
        "--app-token", help="Wolai app token; if missing, read WOLAI_APP_TOKEN env"
    )
    parser.add_argument("--app-id", help="Wolai app id; fallback WOLAI_APP_ID env")
    parser.add_argument(
        "--app-secret", help="Wolai app secret; fallback WOLAI_APP_SECRET env"
    )
    parser.add_argument(
        "--base-url",
        default=os.environ.get("WOLAI_OPENAPI_BASE_URL", "https://openapi.wolai.com/v1"),
        help="Wolai OpenAPI base url",
    )
    return parser.parse_args()


def normalize_base_url(base_url: str) -> str:
    return base_url.rstrip("/")


def parse_json_arg(raw: Optional[str], *, arg_name: str) -> Any:
    if not raw:
        return None
    try:
        return json.loads(raw)
    except json.JSONDecodeError as exc:
        raise WolaiError(
            f"{arg_name} must be valid JSON",
            code="validation",
            detail={"arg": arg_name},
        ) from exc


def build_text_block(content: str) -> Dict[str, Any]:
    return {"type": "text", "content": content}


def request_json(
    method: str,
    url: str,
    token: Optional[str] = None,
    payload: Optional[Dict[str, Any]] = None,
) -> Any:
    body = None
    base_headers = {
        "Accept": "application/json",
    }
    if payload is not None:
        body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        base_headers["Content-Type"] = "application/json; charset=utf-8"

    auth_variants: List[Optional[str]] = [None]
    if token:
        token_value = str(token).strip()
        auth_variants = [token_value]
        if token_value.lower().startswith("bearer "):
            raw_token = token_value[7:].strip()
            if raw_token:
                auth_variants.append(raw_token)
        else:
            auth_variants.append(f"Bearer {token_value}")

    last_http_error: Optional[urllib.error.HTTPError] = None
    for idx, auth in enumerate(auth_variants):
        headers = dict(base_headers)
        if auth:
            headers["Authorization"] = auth
        req = urllib.request.Request(url=url, method=method, data=body, headers=headers)
        try:
            with urllib.request.urlopen(req) as resp:
                raw = resp.read().decode("utf-8")
                if not raw:
                    return {}
                return json.loads(raw)
        except urllib.error.HTTPError as exc:
            last_http_error = exc
            can_retry_auth = idx < len(auth_variants) - 1 and exc.code in (401, 403)
            if can_retry_auth:
                continue

            raw = exc.read().decode("utf-8", errors="replace")
            detail: Dict[str, Any] = {
                "status": exc.code,
                "body": raw,
                "url": url,
                "auth_variant": auth,
            }
            message = f"Wolai API request failed ({exc.code})"
            try:
                err_payload = json.loads(raw) if raw else {}
                if isinstance(err_payload, dict):
                    if err_payload.get("message"):
                        message = str(err_payload["message"])
                    detail["response"] = err_payload
            except json.JSONDecodeError:
                pass
            raise WolaiError(message, code="http_error", detail=detail) from exc
        except urllib.error.URLError as exc:
            raise WolaiError(
                f"network error: {exc.reason}",
                code="network_error",
                detail={"url": url},
            ) from exc

    if last_http_error:
        raise WolaiError(
            f"Wolai API request failed ({last_http_error.code})",
            code="http_error",
            detail={"url": url},
        )
    raise WolaiError("unexpected request flow", code="internal_error", detail={"url": url})


def extract_app_token(payload: Dict[str, Any]) -> Optional[str]:
    app_token = payload.get("app_token")
    if isinstance(app_token, str) and app_token.strip():
        return app_token.strip()

    app_token_obj = payload.get("appToken")
    if isinstance(app_token_obj, str) and app_token_obj.strip():
        return app_token_obj.strip()
    if isinstance(app_token_obj, dict):
        nested = app_token_obj.get("app_token") or app_token_obj.get("appToken")
        if isinstance(nested, str) and nested.strip():
            return nested.strip()

    data_obj = payload.get("data")
    if isinstance(data_obj, dict):
        nested = (
            data_obj.get("app_token")
            or data_obj.get("appToken")
            or ((data_obj.get("appToken") or {}).get("app_token") if isinstance(data_obj.get("appToken"), dict) else None)
        )
        if isinstance(nested, str) and nested.strip():
            return nested.strip()
    return None


def resolve_token(args: argparse.Namespace, base_url: str) -> str:
    explicit = args.app_token or os.environ.get("WOLAI_APP_TOKEN")
    if explicit:
        return explicit

    app_id = args.app_id or os.environ.get("WOLAI_APP_ID")
    app_secret = args.app_secret or os.environ.get("WOLAI_APP_SECRET")
    if not app_id or not app_secret:
        raise WolaiError(
            "missing app token or app credentials (WOLAI_APP_TOKEN / WOLAI_APP_ID + WOLAI_APP_SECRET)",
            code="missing_credentials",
        )

    payload = request_json(
        "POST",
        f"{base_url}/token",
        payload={"appId": app_id, "appSecret": app_secret},
    )
    if not isinstance(payload, dict):
        raise WolaiError("unexpected token response", code="invalid_token_response")
    app_token = extract_app_token(payload)
    if not app_token:
        raise WolaiError(
            "token response missing app token field",
            code="invalid_token_response",
            detail=payload,
        )
    return str(app_token)


def create_block(args: argparse.Namespace, base_url: str, token: str) -> Any:
    if not args.parent_id:
        raise WolaiError("missing --parent-id for create", code="validation")
    blocks = parse_json_arg(args.block_json, arg_name="--block-json")
    if blocks is None:
        if not args.content:
            raise WolaiError(
                "missing --content or --block-json for create", code="validation"
            )
        blocks = build_text_block(args.content)
    return request_json(
        "POST",
        f"{base_url}/blocks",
        token=token,
        payload={"parent_id": args.parent_id, "blocks": blocks},
    )


def read_block(args: argparse.Namespace, base_url: str, token: str) -> Any:
    if not args.id:
        raise WolaiError("missing --id for read", code="validation")
    return request_json("GET", f"{base_url}/blocks/{args.id}", token=token)


def read_children(args: argparse.Namespace, base_url: str, token: str) -> Any:
    target_id = args.id or args.parent_id
    if not target_id:
        raise WolaiError("missing --id or --parent-id for read-children", code="validation")
    query: Dict[str, Any] = {}
    if args.cursor:
        query["cursor"] = args.cursor
    if args.limit:
        query["limit"] = args.limit
    url = f"{base_url}/blocks/{target_id}/children"
    if query:
        url = f"{url}?{urllib.parse.urlencode(query)}"
    return request_json("GET", url, token=token)


def update_block(args: argparse.Namespace, base_url: str, token: str) -> Any:
    if not args.id:
        raise WolaiError("missing --id for update", code="validation")
    blocks = parse_json_arg(args.block_json, arg_name="--block-json")
    if blocks is None:
        if not args.content:
            raise WolaiError(
                "missing --content or --block-json for update", code="validation"
            )
        blocks = build_text_block(args.content)
    payload = {"blocks": blocks}
    # Wolai docs evolve; try PATCH first, then PUT.
    try:
        return request_json(
            "PATCH",
            f"{base_url}/blocks/{args.id}",
            token=token,
            payload=payload,
        )
    except WolaiError as first_exc:
        try:
            return request_json(
                "PUT",
                f"{base_url}/blocks/{args.id}",
                token=token,
                payload=payload,
            )
        except WolaiError as second_exc:
            raise WolaiError(
                "update failed with both PATCH and PUT",
                code="update_failed",
                detail={"patch_error": first_exc.detail, "put_error": second_exc.detail},
            ) from second_exc


def delete_block(args: argparse.Namespace, base_url: str, token: str) -> Any:
    if not args.id:
        raise WolaiError("missing --id for delete", code="validation")
    return request_json("DELETE", f"{base_url}/blocks/{args.id}", token=token)


def search_blocks(args: argparse.Namespace, base_url: str, token: str) -> Any:
    # OpenAPI currently has no dedicated full-text search endpoint in this skill.
    # We do lightweight filtering from children list.
    if not args.parent_id and not args.id:
        raise WolaiError(
            "search requires --parent-id (or --id) as search scope",
            code="validation",
        )
    if not args.query:
        raise WolaiError("missing --query for search", code="validation")
    data = read_children(args, base_url, token)
    if not isinstance(data, dict):
        return {"matches": [], "raw": data}

    records = data.get("data")
    if not isinstance(records, list):
        return {"matches": [], "raw": data}

    needle = args.query.lower()
    matches: List[Dict[str, Any]] = []
    for item in records:
        if not isinstance(item, dict):
            continue
        hay = json.dumps(item, ensure_ascii=False).lower()
        if needle in hay:
            matches.append(item)
    return {
        "matches": matches,
        "total": len(matches),
        "has_more": data.get("has_more"),
        "next_cursor": data.get("next_cursor"),
    }


def run(args: argparse.Namespace) -> Any:
    base_url = normalize_base_url(args.base_url)
    token = resolve_token(args, base_url)
    if args.op == "create":
        return create_block(args, base_url, token)
    if args.op == "read":
        return read_block(args, base_url, token)
    if args.op == "read-children":
        return read_children(args, base_url, token)
    if args.op == "update":
        return update_block(args, base_url, token)
    if args.op == "delete":
        return delete_block(args, base_url, token)
    if args.op == "search":
        return search_blocks(args, base_url, token)
    raise WolaiError(f"unsupported op: {args.op}", code="validation")


def main() -> int:
    args = parse_args()
    try:
        data = run(args)
        out = {"ok": True, "op": args.op, "data": data, "error": None}
    except WolaiError as exc:
        out = {
            "ok": False,
            "op": args.op,
            "data": None,
            "error": {"code": exc.code, "message": str(exc), "detail": exc.detail},
        }
    json.dump(out, sys.stdout, ensure_ascii=False, indent=2)
    sys.stdout.write("\n")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
