"""Delete a predicate from WordLift entities in bulk."""

from __future__ import annotations

import json
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from dataclasses import dataclass
from typing import Any
from urllib.parse import quote

import requests

from worai.core.wordlift import DEFAULT_ENTITIES_ENDPOINT, DEFAULT_GRAPHQL_ENDPOINT, graphql_request

_CURIE_PREFIXES = {
    "schema": "http://schema.org/",
    "seovoc": "https://w3id.org/seovoc/",
    "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
    "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
    "owl": "http://www.w3.org/2002/07/owl#",
    "xsd": "http://www.w3.org/2001/XMLSchema#",
}

_FETCH_QUERY_TEMPLATE = """
query {{
  entities {{
    iri
    value: string(name: {predicate})
  }}
}}
"""


@dataclass
class PropertyDeleteOptions:
    api_key: str
    predicate: str
    graphql_endpoint: str = DEFAULT_GRAPHQL_ENDPOINT
    entities_endpoint: str = DEFAULT_ENTITIES_ENDPOINT
    dry_run: bool = False
    workers: int = 4
    retries: int = 3
    rate_delay: float = 0.0
    limit: int = 0
    timeout: int = 60


@dataclass
class PropertyDeleteSummary:
    predicate: str
    candidates: int
    attempted: int
    removed: int
    failed: int


def normalize_predicate(value: str) -> str:
    token = value.strip()
    if not token:
        raise ValueError("Predicate must not be empty.")

    if token.startswith("http://") or token.startswith("https://"):
        return token

    if ":" not in token:
        raise ValueError(
            "Predicate must be a full IRI or CURIE (for example, seovoc:html)."
        )

    prefix, suffix = token.split(":", 1)
    if not suffix:
        raise ValueError("Predicate CURIE must include a suffix (for example, seovoc:html).")

    iri_prefix = _CURIE_PREFIXES.get(prefix.lower())
    if iri_prefix is None:
        supported = ", ".join(sorted(_CURIE_PREFIXES))
        raise ValueError(f"Unknown CURIE prefix '{prefix}'. Supported prefixes: {supported}.")

    return f"{iri_prefix}{suffix}"


def _json_escape(value: str) -> str:
    return json.dumps(value)


def fetch_candidate_iris(
    *,
    endpoint: str,
    api_key: str,
    predicate_iri: str,
    timeout: int = 60,
) -> list[str]:
    query = _FETCH_QUERY_TEMPLATE.format(predicate=_json_escape(predicate_iri))
    data = graphql_request(endpoint, api_key, query, timeout=timeout, include_private=True)
    try:
        entities = data["data"]["entities"]
    except (KeyError, TypeError) as exc:
        raise RuntimeError("Unexpected GraphQL response structure; 'data.entities' not found") from exc

    out: list[str] = []
    for entry in entities or []:
        if not isinstance(entry, dict):
            continue
        iri = (entry.get("iri") or "").strip()
        value = entry.get("value")
        if not iri or value in (None, ""):
            continue
        out.append(iri)

    return out


def _remove_property_once(
    *,
    entities_endpoint: str,
    api_key: str,
    iri: str,
    predicate_iri: str,
    timeout: int,
) -> tuple[bool, str]:
    encoded_iri = quote(iri, safe="")
    target_url = f"{entities_endpoint}?id={encoded_iri}"
    payload = [{"op": "remove", "path": f"/{predicate_iri}"}]
    headers = {
        "Authorization": f"Key {api_key}",
        "Content-Type": "application/json-patch+json",
        "Accept": "application/ld+json",
        "X-include-Private": "true",
    }
    try:
        resp = requests.patch(target_url, headers=headers, data=json.dumps(payload), timeout=timeout)
    except requests.RequestException as exc:
        return False, str(exc)

    if 200 <= resp.status_code < 300:
        return True, ""
    return False, f"HTTP {resp.status_code}: {resp.text[:500]}"


def _remove_property_with_retries(
    *,
    entities_endpoint: str,
    api_key: str,
    iri: str,
    predicate_iri: str,
    timeout: int,
    retries: int,
    rate_delay: float,
) -> tuple[str, bool, str]:
    attempts = max(1, retries)
    error = ""
    for attempt in range(attempts):
        ok, message = _remove_property_once(
            entities_endpoint=entities_endpoint,
            api_key=api_key,
            iri=iri,
            predicate_iri=predicate_iri,
            timeout=timeout,
        )
        if ok:
            if rate_delay > 0:
                time.sleep(rate_delay)
            return iri, True, ""
        error = message
        if attempt + 1 < attempts:
            time.sleep(1)

    return iri, False, error


def run_property_delete(options: PropertyDeleteOptions) -> PropertyDeleteSummary:
    predicate_iri = normalize_predicate(options.predicate)
    candidates = fetch_candidate_iris(
        endpoint=options.graphql_endpoint,
        api_key=options.api_key,
        predicate_iri=predicate_iri,
        timeout=options.timeout,
    )
    if options.limit > 0:
        candidates = candidates[: options.limit]

    if options.dry_run:
        return PropertyDeleteSummary(
            predicate=predicate_iri,
            candidates=len(candidates),
            attempted=0,
            removed=0,
            failed=0,
        )

    if not candidates:
        return PropertyDeleteSummary(
            predicate=predicate_iri,
            candidates=0,
            attempted=0,
            removed=0,
            failed=0,
        )

    workers = max(1, options.workers)
    removed = 0
    failed = 0

    with ThreadPoolExecutor(max_workers=workers) as executor:
        futures = [
            executor.submit(
                _remove_property_with_retries,
                entities_endpoint=options.entities_endpoint,
                api_key=options.api_key,
                iri=iri,
                predicate_iri=predicate_iri,
                timeout=options.timeout,
                retries=options.retries,
                rate_delay=options.rate_delay,
            )
            for iri in candidates
        ]
        for future in as_completed(futures):
            _iri, ok, _message = future.result()
            if ok:
                removed += 1
            else:
                failed += 1

    return PropertyDeleteSummary(
        predicate=predicate_iri,
        candidates=len(candidates),
        attempted=len(candidates),
        removed=removed,
        failed=failed,
    )
