from __future__ import annotations

from dataclasses import dataclass


@dataclass
class OpenAPIValidationIssue:
    type: str
    message: str
    details: dict | None = None


def _matches_status_key(status_code: int, key: str) -> bool:
    """OpenAPI response keys can be explicit ("200"), wildcard ("2XX"), or "default"."""
    key = str(key).strip().upper()
    if key == "DEFAULT":
        return True
    if len(key) == 3 and key.endswith("XX") and key[0].isdigit():
        return int(key[0]) == int(status_code / 100)
    return key == str(status_code)


def _get_operation(spec: dict, *, path: str, method: str) -> dict | None:
    paths = spec.get("paths") or {}
    op = (paths.get(path) or {}).get(str(method).lower())
    if isinstance(op, dict):
        return op
    return None


def validate_openapi_response(
    *,
    spec: dict,
    path: str,
    method: str,
    status_code: int | None,
    response_headers: dict | None = None,
    response_json=None,
) -> list[OpenAPIValidationIssue]:
    """Validate a response against an OpenAPI operation.

    - Status code must be declared in operation.responses (supports 2XX and default)
    - If response appears to be JSON and jsonschema is installed, validate body

    Returns a list of issues (empty => OK or validation skipped).
    """

    issues: list[OpenAPIValidationIssue] = []

    op = _get_operation(spec, path=path, method=method)
    if not op:
        return [
            OpenAPIValidationIssue(
                type="openapi_operation_missing",
                message=f"Operation not found in spec: {str(method).upper()} {path}",
            )
        ]

    responses = op.get("responses") or {}
    if status_code is None:
        # Network errors etc. Not an OpenAPI mismatch.
        return issues

    # --------------------
    # Status validation
    # --------------------
    declared_keys = [str(k) for k in responses.keys()]
    if declared_keys:
        if not any(_matches_status_key(int(status_code), k) for k in declared_keys):
            issues.append(
                OpenAPIValidationIssue(
                    type="openapi_status",
                    message=f"Status {status_code} not declared in OpenAPI responses: {declared_keys}",
                    details={"status_code": status_code, "declared": declared_keys},
                )
            )

    # --------------------
    # JSON schema validation (optional)
    # --------------------
    ct = ""
    if response_headers and isinstance(response_headers, dict):
        for k, v in response_headers.items():
            if str(k).lower() == "content-type" and v:
                ct = str(v).lower()
                break

    is_json = isinstance(response_json, (dict, list))
    if not is_json:
        # if body wasn't parsed as JSON, skip schema validation
        return issues

    if ct and "json" not in ct:
        # Respect explicit non-JSON content-type
        return issues

    # Find best matching response schema: exact > 2XX > default
    chosen_resp: dict | None = None
    for k in (str(status_code), f"{int(status_code/100)}XX", "default"):
        if k in responses:
            chosen_resp = responses.get(k)
            break

    if not isinstance(chosen_resp, dict):
        return issues

    content = chosen_resp.get("content") or {}
    schema = None
    if isinstance(content, dict):
        app_json = content.get("application/json") or content.get("application/*+json")
        if isinstance(app_json, dict):
            schema = app_json.get("schema")

    if not schema:
        return issues

    try:
        import jsonschema  # type: ignore

        # Resolve in-document refs like #/components/schemas/X
        resolver = jsonschema.RefResolver.from_schema(spec)  # type: ignore[attr-defined]
        validator_cls = jsonschema.validators.validator_for(schema)  # type: ignore[attr-defined]
        validator_cls.check_schema(schema)
        validator = validator_cls(schema, resolver=resolver)
        errors = sorted(validator.iter_errors(response_json), key=lambda e: list(e.path))
        if errors:
            # Keep just the first few errors for signal.
            details = {
                "error_count": len(errors),
                "errors": [
                    {
                        "message": e.message,
                        "path": list(e.path),
                        "schema_path": list(e.schema_path),
                    }
                    for e in errors[:5]
                ],
            }
            issues.append(
                OpenAPIValidationIssue(
                    type="openapi_schema",
                    message="Response JSON does not match OpenAPI schema",
                    details=details,
                )
            )
    except ImportError:
        # Optional dependency not installed: status validation still works.
        return issues
    except Exception as e:
        issues.append(
            OpenAPIValidationIssue(
                type="openapi_schema_error",
                message=f"OpenAPI schema validation failed: {e}",
            )
        )

    return issues
