from __future__ import annotations

import datetime as dt
import uuid
from collections import defaultdict
from typing import Dict, List, Optional, Tuple

import structlog
from google.api_core import exceptions as google_exceptions
from google.cloud import dlp_v2

from app.core.config import get_settings
from app.models.pii import PiiRedactionRequest, PiiRedactionResponse
from app.services import credits
from app.services.credits import InsufficientCreditsError
from app.services.firestore import get_firestore_client


class PiiServiceError(Exception):
    ...


_CLIENT: Optional[dlp_v2.DlpServiceClient] = None
_LOGGER = structlog.get_logger(__name__)


def _get_client() -> dlp_v2.DlpServiceClient:
    global _CLIENT
    if _CLIENT is None:
        _CLIENT = dlp_v2.DlpServiceClient()
    return _CLIENT


def _bytes_to_char_range(text: str, start: int, end: int) -> Tuple[int, int]:
    encoded = text.encode("utf-8")
    prefix = encoded[:start]
    token = encoded[start:end]
    chars_before = len(prefix.decode("utf-8", errors="ignore"))
    char_length = len(token.decode("utf-8", errors="ignore"))
    return chars_before, chars_before + char_length


def _extract_offsets(text: str, finding) -> Tuple[Optional[int], Optional[int]]:
    location = getattr(finding, "location", None)
    if location:
        content_locations = getattr(location, "content_locations", []) or []
        for content_loc in content_locations:
            byte_range = getattr(content_loc, "byte_range", None)
            if byte_range and byte_range.start is not None and byte_range.end is not None:
                return _bytes_to_char_range(text, int(byte_range.start), int(byte_range.end))
            range_loc = getattr(content_loc, "range", None)
            if range_loc and range_loc.start is not None and range_loc.end is not None:
                return int(range_loc.start), int(range_loc.end)
    quote = getattr(finding, "quote", "")
    if quote:
        idx = text.find(quote)
        if idx != -1:
            return idx, idx + len(quote)
    return None, None


def _build_tokens(text: str, findings) -> Tuple[str, Dict[str, List[str]]]:
    replacements: List[Tuple[int, int, str, str, str]] = []
    entities: Dict[str, List[str]] = defaultdict(list)

    processed = []
    for finding in findings:
        info_type = getattr(getattr(finding, "info_type", None), "name", "UNKNOWN")
        quote = getattr(finding, "quote", "") or ""
        start, end = _extract_offsets(text, finding)
        if quote == "" or start is None or end is None:
            continue
        info_key = info_type.upper()
        index = len(entities[info_key]) + 1
        token = f"<PII_{info_key}_{index}>"
        replacements.append((start, end, token, info_key, quote))
        entities[info_key].append(quote)
        processed.append((start, end))

    if not replacements:
        return text, {}

    replacements.sort(key=lambda item: item[0])
    result_parts: List[str] = []
    cursor = 0
    for start, end, token, _info_key, _quote in replacements:
        if start < cursor:
            # Overlapping finding – skip to avoid corrupting text
            continue
        result_parts.append(text[cursor:start])
        result_parts.append(token)
        cursor = end
    result_parts.append(text[cursor:])

    anonymized_text = "".join(result_parts)
    return anonymized_text, dict(entities)


def _inspect_text(parent: str, text: str):
    client = _get_client()
    request = {
        "parent": parent,
        "item": {"value": text},
        "inspect_config": {"include_quote": True},
    }
    return client.inspect_content(request=request)


def _save_redaction_history(
    *,
    user_id: str,
    redaction_id: str,
    original_text: str,
    anonymized_text: str,
    entities: Dict[str, List[str]],
    credits_used: int,
    timestamp: dt.datetime,
    source: str,
) -> None:
    try:
        db = get_firestore_client()
        payload = {
            "userId": user_id,
            "redactionId": redaction_id,
            "originalTextLength": len(original_text),
            "anonymizedTextLength": len(anonymized_text),
            "entityCount": sum(len(values) for values in entities.values()),
            "entityTypes": list(entities.keys()),
            "timestamp": timestamp,
            "creditsUsed": credits_used,
            "source": source,
            "createdAt": dt.datetime.now(dt.timezone.utc),
        }
        db.collection("piiRedactions").add(payload)
    except Exception as exc:  # pragma: no cover
        _LOGGER.warning("pii_redaction.history_save_failed", error=str(exc))


async def redact_text(
    request: PiiRedactionRequest,
    user_id: str,
    source: str = "api",
    *,
    api_key: Optional[str] = None,
) -> PiiRedactionResponse:
    if not request.text:
        raise PiiServiceError("No text provided")

    try:
        credits.deduct_pii_redaction_credits(user_id)
    except InsufficientCreditsError as exc:
        raise PiiServiceError("Insufficient credits for PII redaction") from exc

    settings = get_settings()
    project = settings.dlp.project_id or settings.firebase.project_id
    if not project:
        raise PiiServiceError("DLP project ID must be configured")

    parent = f"projects/{project}/locations/{settings.dlp.location}"

    try:
        response = _inspect_text(parent, request.text)
    except google_exceptions.PermissionDenied as exc:
        raise PiiServiceError(
            "DLP API is not enabled for this project. Enable dlp.googleapis.com and retry."
        ) from exc
    except google_exceptions.GoogleAPICallError as exc:
        raise PiiServiceError("DLP inspection failed") from exc

    findings = getattr(getattr(response, "result", None), "findings", []) or []
    anonymized_text, entity_map = _build_tokens(request.text, findings)
    timestamp = dt.datetime.now(dt.timezone.utc)
    credits_used = credits.CREDIT_COSTS["pii_redaction"]

    if user_id:
        redaction_id = uuid.uuid4().hex[:12]
        _save_redaction_history(
            user_id=user_id,
            redaction_id=redaction_id,
            original_text=request.text,
            anonymized_text=anonymized_text,
            entities=entity_map,
            credits_used=credits_used,
            timestamp=timestamp,
            source=source,
        )

    return PiiRedactionResponse(
        anonymized_text=anonymized_text,
        entities=entity_map,
        credits_used=credits_used,
        timestamp=timestamp,
    )
