# retoor <retoor@molodetz.nl>

import json
import logging
from typing import Any, Optional

from devplacepy.utils import strip_html, generate_uid, client_ip
from devplacepy.services.audit import store
from devplacepy.services.audit.categories import category_for
from devplacepy.services.background import background

logger = logging.getLogger(__name__)

_UNSET = object()
SUMMARY_LIMIT = 140


def link(
    relation: str,
    object_type: str,
    object_uid: Optional[str],
    object_label: Optional[str] = None,
) -> dict:
    return {
        "relation": relation,
        "object_type": object_type,
        "object_uid": object_uid,
        "object_label": object_label,
    }


def actor(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("actor", "user", uid, label)


def target(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("target", object_type, uid, label)


def parent(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("parent", object_type, uid, label)


def author(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("author", "user", uid, label)


def recipient(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("recipient", "user", uid, label)


def mention(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("mention", "user", uid, label)


def project(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("project", "project", uid, label)


def attachment_link(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("attachment", "attachment", uid, label)


def source(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("source", object_type, uid, label)


def destination(object_type: str, uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("destination", object_type, uid, label)


def schedule(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("schedule", "schedule", uid, label)


def instance(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("instance", "instance", uid, label)


def setting(key: Optional[str], label: Optional[str] = None) -> dict:
    return link("setting", "setting", key, label or key)


def service_link(name: Optional[str], label: Optional[str] = None) -> dict:
    return link("target", "service", name, label or name)


def job(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("job", "job", uid, label)


def poll(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("poll", "poll", uid, label)


def option(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("option", "poll_option", uid, label)


def task(uid: Optional[str], label: Optional[str] = None) -> dict:
    return link("target", "task", uid, label)


def _actor_from(user: Optional[dict]) -> dict:
    if not user:
        return {
            "actor_kind": "guest",
            "actor_uid": None,
            "actor_username": None,
            "actor_role": "guest",
        }
    return {
        "actor_kind": "user",
        "actor_uid": user.get("uid"),
        "actor_username": user.get("username"),
        "actor_role": "admin" if user.get("role") == "Admin" else "member",
    }


def _request_fields(request) -> dict:
    fields: dict = {
        "request_method": None,
        "request_path": None,
        "actor_ip": None,
        "user_agent": None,
        "devii": False,
    }
    if request is None:
        return fields
    try:
        fields["request_method"] = getattr(request, "method", None)
        url = getattr(request, "url", None)
        fields["request_path"] = url.path if url is not None else None
        fields["actor_ip"] = client_ip(request, default=None)
        headers = getattr(request, "headers", None)
        if headers is not None:
            fields["user_agent"] = headers.get("user-agent")
            fields["devii"] = headers.get("x-devii-agent") == "1"
    except Exception as exc:
        logger.debug("audit request field read failed: %s", exc)
    return fields


def _sanitize_summary(text: Optional[str], limit: int = SUMMARY_LIMIT) -> Optional[str]:
    if not text:
        return None
    cleaned = strip_html(str(text))
    if not cleaned:
        return None
    if len(cleaned) > limit:
        return cleaned[: limit - 3].rstrip() + "..."
    return cleaned


def _coerce_scalar(value: Any) -> Optional[str]:
    if value is None:
        return None
    if isinstance(value, bool):
        return "1" if value else "0"
    return str(value)


def _persist(row: dict, links: list[dict]) -> None:
    audit_uid = store.insert_event(row)
    store.insert_links(audit_uid, links)


def _write(
    *,
    event_key: str,
    base_actor: dict,
    request_fields: dict,
    target_type: Optional[str],
    target_uid: Optional[str],
    target_label: Optional[str],
    old_value: Any,
    new_value: Any,
    summary: Optional[str],
    metadata: Optional[dict],
    result: str,
    origin: Optional[str],
    via_agent: Optional[int],
    links: Optional[list[dict]],
    category: Optional[str],
) -> Optional[str]:
    resolved_origin = origin
    if resolved_origin is None:
        resolved_origin = "devii" if request_fields.get("devii") else "web"
    resolved_via = via_agent
    if resolved_via is None:
        resolved_via = 1 if request_fields.get("devii") else 0
    audit_uid = generate_uid()
    row = {
        "uid": audit_uid,
        "created_at": store.now(),
        "event_key": event_key,
        "category": category or category_for(event_key),
        "actor_kind": base_actor.get("actor_kind"),
        "actor_uid": base_actor.get("actor_uid"),
        "actor_username": base_actor.get("actor_username"),
        "actor_role": base_actor.get("actor_role"),
        "origin": resolved_origin,
        "via_agent": int(resolved_via),
        "request_method": request_fields.get("request_method"),
        "request_path": request_fields.get("request_path"),
        "actor_ip": request_fields.get("actor_ip"),
        "user_agent": request_fields.get("user_agent"),
        "target_type": target_type,
        "target_uid": target_uid,
        "target_label": _sanitize_summary(target_label, 200),
        "old_value": _coerce_scalar(old_value),
        "new_value": _coerce_scalar(new_value),
        "summary": _sanitize_summary(summary),
        "metadata": json.dumps(metadata) if metadata else None,
        "result": result,
    }
    all_links = list(links or [])
    if base_actor.get("actor_uid") and not any(
        item.get("relation") == "actor" for item in all_links
    ):
        all_links.append(
            actor(base_actor["actor_uid"], base_actor.get("actor_username"))
        )
    background.submit(_persist, row, all_links)
    logger.debug(
        "audit %s by %s/%s result=%s origin=%s",
        event_key,
        row["actor_kind"],
        row["actor_username"],
        result,
        resolved_origin,
    )
    return audit_uid


def record(
    request,
    event_key: str,
    *,
    user=_UNSET,
    actor_kind: Optional[str] = None,
    target_type: Optional[str] = None,
    target_uid: Optional[str] = None,
    target_label: Optional[str] = None,
    old_value: Any = None,
    new_value: Any = None,
    summary: Optional[str] = None,
    metadata: Optional[dict] = None,
    result: str = "success",
    origin: Optional[str] = None,
    via_agent: Optional[int] = None,
    links: Optional[list[dict]] = None,
    category: Optional[str] = None,
) -> Optional[str]:
    try:
        if user is _UNSET:
            from devplacepy.utils import get_current_user

            try:
                user = get_current_user(request)
            except Exception:
                user = None
        base_actor = _actor_from(user)
        if actor_kind:
            base_actor["actor_kind"] = actor_kind
        return _write(
            event_key=event_key,
            base_actor=base_actor,
            request_fields=_request_fields(request),
            target_type=target_type,
            target_uid=target_uid,
            target_label=target_label,
            old_value=old_value,
            new_value=new_value,
            summary=summary,
            metadata=metadata,
            result=result,
            origin=origin,
            via_agent=via_agent,
            links=links,
            category=category,
        )
    except Exception as exc:
        logger.warning("audit.record failed for %s: %s", event_key, exc)
        return None


def record_system(
    event_key: str,
    *,
    actor_kind: str = "system",
    actor_uid: Optional[str] = None,
    actor_username: Optional[str] = None,
    actor_role: str = "system",
    target_type: Optional[str] = None,
    target_uid: Optional[str] = None,
    target_label: Optional[str] = None,
    old_value: Any = None,
    new_value: Any = None,
    summary: Optional[str] = None,
    metadata: Optional[dict] = None,
    result: str = "success",
    origin: str = "service",
    via_agent: int = 0,
    links: Optional[list[dict]] = None,
    category: Optional[str] = None,
) -> Optional[str]:
    try:
        base_actor = {
            "actor_kind": actor_kind,
            "actor_uid": actor_uid,
            "actor_username": actor_username,
            "actor_role": actor_role,
        }
        return _write(
            event_key=event_key,
            base_actor=base_actor,
            request_fields={},
            target_type=target_type,
            target_uid=target_uid,
            target_label=target_label,
            old_value=old_value,
            new_value=new_value,
            summary=summary,
            metadata=metadata,
            result=result,
            origin=origin,
            via_agent=via_agent,
            links=links,
            category=category,
        )
    except Exception as exc:
        logger.warning("audit.record_system failed for %s: %s", event_key, exc)
        return None
