"""Policy, lifecycle, token, rendering and resolution orchestration."""
from __future__ import annotations

import base64, hashlib, html, io, json, os, re, secrets, threading, time
from datetime import datetime, timedelta
from typing import Any
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError

from config.control_plane import get_control_plane_session
from bridge_platform.apps.registry import load_manifest
from bridge_platform.interapp.client import call_app
from bridge_platform.logging.audit_logger import AuditLogger
from bridge_platform.quotas.service import QuotaExceeded, consume
from bridge_platform.secrets.secrets_manager import SecretsManager
from .models import QRAuditEvent, QRAppPermission, QRIdempotency, QRRecord, QRResolution

KINDS={"permanent_revocable","fixed_expiry","relative_expiry","single_use","limited_use","limited_scan","dynamic_session"}
STATES={"draft","active","suspended","expired","consumed","revoked","replaced"}
TRANSITIONS={"draft":{"active","revoked"},"active":{"suspended","expired","consumed","revoked","replaced"},"suspended":{"active","revoked","replaced"}}
IDENT=re.compile(r"^[a-z][a-z0-9_]{1,63}$")
PUBLIC_ERROR="This QR code is invalid or no longer available."
_PUBLIC_RATE: dict[str,list[float]]={}; _PUBLIC_RATE_LOCK=threading.Lock()

class QRServiceError(Exception):
    def __init__(self, code:str, message:str, status:int=400): self.code,self.status=code,status; super().__init__(message)

def token_pair() -> tuple[str,str,str]:
    token=secrets.token_urlsafe(32)
    return token, hashlib.sha256(token.encode()).hexdigest(), token[:8]

def token_hash(token:str)->str: return hashlib.sha256(str(token).encode()).hexdigest()

def _tenant(c):
    value=str(((c.get("tenant") or {}).get("tenant_id")) or "")
    if not value: raise QRServiceError("tenant_context_missing","Trusted tenant context is required.",403)
    return value

def _caller(c):
    value=str(c.get("requesting_app") or "")
    if not value: raise QRServiceError("caller_context_missing","Trusted caller context is required.",403)
    return value

def _admin(c):
    ident=c.get("identity"); return bool(ident and {"admin","administrator","platform_admin"}&set(getattr(ident,"roles",())))

def _dt(value):
    if value in (None,""): return None
    if isinstance(value,datetime): return value.replace(tzinfo=None)
    try: return datetime.fromisoformat(str(value).replace("Z","+00:00")).replace(tzinfo=None)
    except ValueError as exc: raise QRServiceError("invalid_policy","Invalid date/time value.") from exc

def _clean_metadata(value):
    if value is None: return {}
    if not isinstance(value,dict) or len(value)>20: raise QRServiceError("invalid_metadata","Metadata must be an object with at most 20 fields.")
    clean={}
    for key,item in value.items():
        if not re.fullmatch(r"[a-zA-Z][a-zA-Z0-9_.-]{0,63}",str(key)): raise QRServiceError("invalid_metadata","Metadata key is invalid.")
        if isinstance(item,(dict,list)) or len(str(item))>256: raise QRServiceError("invalid_metadata","Metadata values must be bounded scalars.")
        clean[str(key)]=item
    if len(json.dumps(clean))>4096: raise QRServiceError("invalid_metadata","Metadata is too large.")
    return clean

def _policy(value):
    p=dict(value or {}); kind=str(p.get("kind") or "permanent_revocable")
    if kind not in KINDS: raise QRServiceError("invalid_policy","Unsupported QR policy kind.")
    allowed={"kind","valid_from","expires_at","expires_after_seconds","expiration_anchor","max_scans","max_successful_uses","single_use","revoke_after_success","requires_authentication","authentication_purpose","allowed_requesting_apps","allowed_action_keys","allowed_time_window","rate_limit_profile","resolution_ttl_seconds","consume_on"}
    if set(p)-allowed: raise QRServiceError("invalid_policy","Policy contains unsupported fields.")
    for field in ("max_scans","max_successful_uses","expires_after_seconds","resolution_ttl_seconds"):
        if p.get(field) is not None:
            p[field]=int(p[field])
            if p[field]<=0: raise QRServiceError("invalid_policy",f"{field} must be positive.")
    p["kind"]=kind; p["resolution_ttl_seconds"]=min(p.get("resolution_ttl_seconds",300),900)
    if p.get("consume_on") not in (None,"scan","successful_use"): raise QRServiceError("invalid_policy","Invalid consume_on value.")
    if kind=="single_use": p.setdefault("single_use",True); p.setdefault("max_successful_uses",1)
    if kind=="limited_use" and not p.get("max_successful_uses"): raise QRServiceError("invalid_policy","limited_use requires max_successful_uses.")
    if kind=="limited_scan" and not p.get("max_scans"): raise QRServiceError("invalid_policy","limited_scan requires max_scans.")
    if kind=="fixed_expiry" and not p.get("expires_at"): raise QRServiceError("invalid_policy","fixed_expiry requires expires_at.")
    if kind in {"relative_expiry","dynamic_session"} and not p.get("expires_after_seconds"): raise QRServiceError("invalid_policy","Relative policy requires expires_after_seconds.")
    return p

class QRService:
    def __init__(self, session_factory=get_control_plane_session, public_base_url=None, secrets_manager=None):
        self.sessions=session_factory; self.public_base_url=(public_base_url or os.getenv("QR_PUBLIC_BASE_URL","https://portal.example.com")).rstrip("/"); self.secrets=secrets_manager or SecretsManager()

    def create(self,c,p):
        tenant,caller=_tenant(c),_caller(c); self._require_key(p); policy=_policy(p.get("policy")); self._validate_resource(c,p,caller)
        replay=self._replay(tenant,caller,"create_qr_v1",p)
        if replay: return replay|{"idempotent_replay":True}
        token,digest,prefix=token_pair(); now=datetime.utcnow(); status=str(p.get("status") or "active")
        if status not in {"draft","active"}: raise QRServiceError("invalid_state","Initial state must be draft or active.")
        valid=_dt(policy.get("valid_from")); expires=_dt(policy.get("expires_at"))
        if policy.get("expires_after_seconds") and policy.get("expiration_anchor","created_at")=="created_at": expires=now+timedelta(seconds=policy["expires_after_seconds"])
        row=QRRecord(tenant_id=tenant,display_name=self._text(p,"display_name",160,True),description=self._text(p,"description",500),public_token_hash=digest,public_token_prefix=prefix,qr_kind=policy["kind"],resource_type=str(p["resource_type"]),resource_reference=str(p["resource_reference"]),owner_app=str(p["owner_app"]),action_key=str(p.get("action_key") or "") or None,policy_json=policy,status=status,valid_from=valid,expires_at=expires,max_scans=policy.get("max_scans"),max_successful_uses=policy.get("max_successful_uses"),requires_authentication=bool(policy.get("requires_authentication")),authentication_purpose=str(policy.get("authentication_purpose") or "") or None,created_by=str(c.get("user_id") or "") or None,activated_at=now if status=="active" else None,metadata_json=_clean_metadata(p.get("metadata")))
        with self.sessions() as s: s.add(row); s.commit(); s.refresh(row)
        self.secrets.put_secret(tenant_id=tenant,app_id="qr_service",secret_name=f"public_token.{row.qr_id}",secret_value=token,actor_id=str(c.get("user_id") or caller))
        result=self._data(row)|{"public_url":f"{self.public_base_url}/q/{token}","render":{"png_available":True,"svg_available":True}}
        self._save_idempotency(tenant,caller,"create_qr_v1",p,result); self._audit(c,"qr.created",row)
        return result

    def get(self,c,qr_id):
        row=self._record(_tenant(c),qr_id); self._authorize_record(c,row,"read"); return self._data(row,detail=True)

    def list(self,c,p):
        tenant=_tenant(c); caller=_caller(c)
        with self.sessions() as s:
            q=select(QRRecord).where(QRRecord.tenant_id==tenant)
            if not _admin(c): q=q.where(QRRecord.owner_app==caller)
            if p.get("owner_app"): q=q.where(QRRecord.owner_app==str(p["owner_app"]))
            if p.get("resource_type"): q=q.where(QRRecord.resource_type==str(p["resource_type"]))
            if p.get("resource_reference"): q=q.where(QRRecord.resource_reference==str(p["resource_reference"]))
            return {"items":[self._data(x) for x in s.execute(q.order_by(QRRecord.created_at.desc()).limit(min(int(p.get("limit",50)),100))).scalars()],"limit":min(int(p.get("limit",50)),100)}

    def transition(self,c,qr_id,target,reason=None):
        tenant=_tenant(c); caller=_caller(c)
        with self.sessions() as s:
            row=s.execute(select(QRRecord).where(QRRecord.tenant_id==tenant,QRRecord.qr_id==qr_id).with_for_update()).scalar_one_or_none()
            if not row: raise QRServiceError("qr_not_found","QR record was not found.",404)
            self._authorize_record(c,row,target)
            if target not in TRANSITIONS.get(row.status,set()): raise QRServiceError("invalid_transition",f"Cannot transition {row.status} to {target}.",409)
            row.status=target; now=datetime.utcnow()
            if target=="active":
                row.activated_at=now; row.suspended_at=None
                policy=row.policy_json or {}
                if policy.get("expires_after_seconds") and policy.get("expiration_anchor")=="activated_at" and row.expires_at is None: row.expires_at=now+timedelta(seconds=int(policy["expires_after_seconds"]))
            if target=="suspended": row.suspended_at=now
            if target=="revoked": row.revoked_at=now; row.revoked_by=str(c.get("user_id") or caller); row.revoke_reason=self._text({"x":reason},"x",255)
            s.commit(); s.refresh(row)
        self._audit(c,f"qr.{ 'reactivated' if target=='active' else target}",row); return self._data(row,detail=True)

    def revoke(self,c,p):
        self._require_key(p); tenant,caller=_tenant(c),_caller(c); replay=self._replay(tenant,caller,"revoke_qr_v1",p)
        if replay:return replay|{"idempotent_replay":True}
        result=self.transition(c,str(p.get("qr_id") or ""),"revoked",p.get("reason")); self._save_idempotency(tenant,caller,"revoke_qr_v1",p,result); return result

    def regenerate(self,c,p):
        self._require_key(p); old=self._record(_tenant(c),str(p.get("qr_id") or "")); self._authorize_record(c,old,"regenerate")
        replay=self._replay(old.tenant_id,_caller(c),"regenerate_qr_v1",p)
        if replay:return replay|{"idempotent_replay":True}
        create_payload={"display_name":old.display_name,"description":old.description,"resource_type":old.resource_type,"resource_reference":old.resource_reference,"owner_app":old.owner_app,"action_key":old.action_key,"policy":old.policy_json,"metadata":old.metadata_json,"status":"active","idempotency_key":p["idempotency_key"]+":replacement"}
        new=self.create(c,create_payload)
        with self.sessions() as s:
            locked=s.execute(select(QRRecord).where(QRRecord.tenant_id==old.tenant_id,QRRecord.qr_id==old.qr_id).with_for_update()).scalar_one()
            if locked.status not in {"active","suspended","draft"}: raise QRServiceError("invalid_transition","QR can no longer be replaced.",409)
            locked.status="replaced"; locked.replaced_by_qr_id=new["qr_id"]
            replacement=s.get(QRRecord,new["qr_id"]); replacement.replaces_qr_id=locked.qr_id; s.commit()
        result={"old_qr_id":old.qr_id,"new_qr":new}; self._save_idempotency(old.tenant_id,_caller(c),"regenerate_qr_v1",p,result); self._audit(c,"qr.replaced",old,resolution_id=None)
        return result

    def update_policy(self,c,p):
        tenant=_tenant(c); qr_id=str(p.get("qr_id") or ""); policy=_policy(p.get("policy")); row=self._record(tenant,qr_id); self._authorize_record(c,row,"policy")
        with self.sessions() as s:
            current=s.execute(select(QRRecord).where(QRRecord.tenant_id==tenant,QRRecord.qr_id==qr_id).with_for_update()).scalar_one()
            if current.status not in {"draft","active","suspended"}:raise QRServiceError("invalid_transition","Terminal QR policy cannot be changed.",409)
            current.policy_json=policy; current.qr_kind=policy["kind"]; current.valid_from=_dt(policy.get("valid_from")); current.expires_at=_dt(policy.get("expires_at")); current.max_scans=policy.get("max_scans"); current.max_successful_uses=policy.get("max_successful_uses"); current.requires_authentication=bool(policy.get("requires_authentication")); current.authentication_purpose=str(policy.get("authentication_purpose") or "") or None
            s.commit();s.refresh(current)
        self._audit(c,"qr.policy_updated",current);return self._data(current,detail=True)

    def render(self,c,p):
        row=self._record(_tenant(c),str(p.get("qr_id") or "")); self._authorize_record(c,row,"render")
        token=str(p.get("public_token") or self.secrets.get_secret(tenant_id=row.tenant_id,app_id="qr_service",secret_name=f"public_token.{row.qr_id}") or "")
        if not token or not secrets.compare_digest(token_hash(token),row.public_token_hash): raise QRServiceError("token_required","The original token is required to re-render securely.",400)
        fmt=str(p.get("format") or "png"); size=max(128,min(int(p.get("size",512)),1024)); url=f"{self.public_base_url}/q/{token}"
        try:
            import qrcode
            from qrcode.image.svg import SvgPathImage
            qr=qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_M,box_size=max(2,size//41),border=max(1,min(int(p.get("margin",4)),8))); qr.add_data(url); qr.make(fit=True)
            out=io.BytesIO(); qr.make_image(image_factory=SvgPathImage if fmt=="svg" else None).save(out)
        except ImportError as exc: raise QRServiceError("renderer_unavailable","QR rendering dependency is unavailable.",503) from exc
        if fmt not in {"png","svg"}: raise QRServiceError("invalid_render_options","Format must be png or svg.")
        self._audit(c,"qr.rendered",row); return {"qr_id":row.qr_id,"format":fmt,"content_type":"image/svg+xml" if fmt=="svg" else "image/png","filename":f"qr-{row.public_token_prefix}.{fmt}","content_base64":base64.b64encode(out.getvalue()).decode("ascii")}

    def begin(self,c,p,public=False):
        token=str(p.get("token") or ""); self._rate(c,"public_resolution" if public else "resolution",token_hash(token)[:16])
        with self.sessions() as s:
            row=s.execute(select(QRRecord).where(QRRecord.public_token_hash==token_hash(token)).with_for_update()).scalar_one_or_none()
            if not row: raise QRServiceError("qr_unavailable",PUBLIC_ERROR,404)
            if not public and row.tenant_id!=_tenant(c): raise QRServiceError("qr_unavailable",PUBLIC_ERROR,404)
            if not public:self._authorize_record(c,row,"resolve")
            self._evaluate(row,p,scan=True); row.scan_count+=1; row.last_scanned_at=datetime.utcnow()
            rtok,rhash,_=token_pair(); ttl=int((row.policy_json or {}).get("resolution_ttl_seconds",300)); resolution=QRResolution(tenant_id=row.tenant_id,qr_id=row.qr_id,resolution_token_hash=rhash,owner_app=row.owner_app,action_key=row.action_key,resource_type=row.resource_type,resource_reference=row.resource_reference,status="pending_authentication" if row.requires_authentication else "authorized",expires_at=datetime.utcnow()+timedelta(seconds=ttl),correlation_id=str(c.get("request_id") or secrets.token_hex(12)))
            s.add(resolution); s.commit(); s.refresh(resolution)
        data={"resolution_id":resolution.resolution_id,"resolution_token":rtok,"status":resolution.status,"expires_at":resolution.expires_at.isoformat()+"Z","authentication_required":row.requires_authentication}
        self._audit(c,"qr.scanned",row,resolution.resolution_id)
        if row.requires_authentication:
            challenge=self._create_challenge(c,row,resolution,p); data["challenge_id"]=challenge
        self._audit(c,"qr.resolution_started",row,resolution.resolution_id); return data

    def continue_resolution(self,c,p):
        resolution=self._resolution_by_token(str(p.get("resolution_token") or ""));
        if resolution.status!="pending_authentication" or resolution.expires_at<=datetime.utcnow(): raise QRServiceError("resolution_unavailable","Resolution is invalid or expired.",410)
        result=self._verify_challenge(c,resolution,p)
        if not result.get("verified"): raise QRServiceError("authentication_failed","Authentication could not be completed.",403)
        with self.sessions() as s:
            current=s.execute(select(QRResolution).where(QRResolution.resolution_id==resolution.resolution_id).with_for_update()).scalar_one(); row=s.get(QRRecord,current.qr_id); self._evaluate(row,{},scan=False)
            current.status="authorized"; current.authenticated_subject_id=str(result.get("subject_id") or "") or None; s.commit(); s.refresh(current)
        self._audit(c,"qr.authentication_completed",row,current.resolution_id); return {"resolution_id":current.resolution_id,"status":"authorized","expires_at":current.expires_at.isoformat()+"Z"}

    def status(self,c,p):
        r=self._resolution_by_token(str(p.get("resolution_token") or "")); self._authorize_resolution(c,r,consume=False)
        return {"resolution_id":r.resolution_id,"status":"expired" if r.expires_at<=datetime.utcnow() else r.status,"expires_at":r.expires_at.isoformat()+"Z","owner_app":r.owner_app,"action_key":r.action_key}

    def consume_resolution(self,c,p):
        self._require_key(p); tenant,caller=_tenant(c),_caller(c); replay=self._replay(tenant,caller,"consume_resolution_v1",p)
        if replay:return replay|{"idempotent_replay":True}
        digest=token_hash(str(p.get("resolution_token") or ""))
        with self.sessions() as s:
            r=s.execute(select(QRResolution).where(QRResolution.resolution_token_hash==digest).with_for_update()).scalar_one_or_none()
            if not r or r.tenant_id!=tenant: raise QRServiceError("resolution_unavailable","Resolution is invalid or expired.",404)
            self._authorize_resolution(c,r,consume=True)
            if r.status!="authorized" or r.expires_at<=datetime.utcnow(): raise QRServiceError("resolution_unavailable","Resolution is invalid, consumed, or expired.",410)
            row=s.execute(select(QRRecord).where(QRRecord.qr_id==r.qr_id).with_for_update()).scalar_one(); self._evaluate(row,{},scan=False)
            r.status="consumed"; r.consumed_at=datetime.utcnow(); row.successful_use_count+=1; row.last_successful_use_at=r.consumed_at
            if (row.policy_json or {}).get("single_use") or (row.max_successful_uses and row.successful_use_count>=row.max_successful_uses): row.status="consumed"
            result={"resolution_id":r.resolution_id,"owner_app":r.owner_app,"action_key":r.action_key,"resource_type":r.resource_type,"resource_reference":r.resource_reference,"authenticated_subject_id":r.authenticated_subject_id,"consumed_at":r.consumed_at.isoformat()+"Z"}; s.commit()
        self._save_idempotency(tenant,caller,"consume_resolution_v1",p,result); self._audit(c,"qr.resolution_consumed",row,r.resolution_id); return result

    def audit(self,c,qr_id):
        row=self._record(_tenant(c),qr_id); self._authorize_record(c,row,"audit")
        with self.sessions() as s: events=list(s.execute(select(QRAuditEvent).where(QRAuditEvent.tenant_id==row.tenant_id,QRAuditEvent.qr_id==row.qr_id).order_by(QRAuditEvent.created_at.desc()).limit(200)).scalars())
        return {"items":[{"event":e.event,"result":e.result,"requesting_app":e.requesting_app,"correlation_id":e.correlation_id,"created_at":e.created_at.isoformat()+"Z","details":e.details_json} for e in events]}

    def configure_permission(self,c,p):
        if not _admin(c): raise QRServiceError("permission_denied","Administrator role is required.",403)
        app=str(p.get("requesting_app") or "");
        if not IDENT.fullmatch(app) or load_manifest(app) is None: raise QRServiceError("invalid_app","Requesting app is not registered.")
        with self.sessions() as s:
            row=s.execute(select(QRAppPermission).where(QRAppPermission.tenant_id==_tenant(c),QRAppPermission.requesting_app==app)).scalar_one_or_none() or QRAppPermission(tenant_id=_tenant(c),requesting_app=app); s.add(row)
            row.enabled=bool(p.get("enabled",True)); row.operations_json=list(p.get("operations") or []); row.resource_types_json=list(p.get("resource_types") or []); row.action_keys_json=list(p.get("action_keys") or []); row.may_manage_other_apps=bool(p.get("may_manage_other_apps",False)); s.commit()
        return {"requesting_app":app,"enabled":row.enabled}

    def _validate_resource(self,c,p,caller):
        owner=str(p.get("owner_app") or ""); resource=str(p.get("resource_type") or ""); action=str(p.get("action_key") or "")
        if not IDENT.fullmatch(owner) or load_manifest(owner) is None: raise QRServiceError("invalid_owner_app","Owner app is not registered.")
        if not IDENT.fullmatch(resource): raise QRServiceError("invalid_resource_type","Resource type is invalid.")
        ref=str(p.get("resource_reference") or "");
        if not 1<=len(ref)<=255 or any(ord(x)<32 for x in ref): raise QRServiceError("invalid_resource_reference","Resource reference is invalid.")
        owner_manifest=load_manifest(owner)
        if action and (not IDENT.fullmatch(action) or not action.endswith("_v1") or action not in owner_manifest.actions): raise QRServiceError("invalid_action_key","Action key is not registered by the owner app.")
        permission=self._permission(_tenant(c),caller)
        if not _admin(c):
            if not permission or not permission.enabled or "create" not in permission.operations_json: raise QRServiceError("permission_denied","Caller may not create QR records.",403)
            if owner!=caller and not permission.may_manage_other_apps: raise QRServiceError("permission_denied","Caller may not create QR for another app.",403)
            if permission.resource_types_json and resource not in permission.resource_types_json: raise QRServiceError("permission_denied","Resource type is not allowed.",403)
            if action and permission.action_keys_json and action not in permission.action_keys_json: raise QRServiceError("permission_denied","Action key is not allowed.",403)

    def _evaluate(self,row,p,scan):
        now=datetime.utcnow(); policy=row.policy_json or {}
        if row.status!="active": raise QRServiceError("qr_unavailable",PUBLIC_ERROR,410)
        if row.valid_from and now<row.valid_from: raise QRServiceError("qr_unavailable",PUBLIC_ERROR,410)
        if row.expires_at and now>=row.expires_at: self._expire(row.qr_id); raise QRServiceError("qr_unavailable",PUBLIC_ERROR,410)
        if scan and row.max_scans is not None and row.scan_count>=row.max_scans: raise QRServiceError("qr_unavailable",PUBLIC_ERROR,410)
        if row.max_successful_uses is not None and row.successful_use_count>=row.max_successful_uses: raise QRServiceError("qr_unavailable",PUBLIC_ERROR,410)
        allowed=policy.get("allowed_requesting_apps") or []
        if allowed and p.get("requesting_app") not in allowed: raise QRServiceError("qr_unavailable",PUBLIC_ERROR,403)
        window=policy.get("allowed_time_window")
        if window:
            try:start,end=(datetime.strptime(str(x),"%H:%M").time() for x in window); current=now.time(); inside=start<=current<=end if start<=end else current>=start or current<=end
            except (TypeError,ValueError):raise QRServiceError("invalid_policy","Invalid allowed_time_window.")
            if not inside:raise QRServiceError("qr_unavailable",PUBLIC_ERROR,410)

    def _expire(self,qr_id):
        with self.sessions() as s:
            r=s.get(QRRecord,qr_id)
            if r and r.status=="active": r.status="expired"; s.commit()

    def _create_challenge(self,c,row,r,p):
        app=os.getenv("QR_CHALLENGE_APP","identity_service")
        if load_manifest(app) is None: raise QRServiceError("challenge_service_unavailable","Authentication challenge service is not configured.",503)
        result=call_app(c,app,"create_verification_challenge_v1",{"purpose":row.authentication_purpose,"pending_resolution_id":r.resolution_id,"correlation_id":r.correlation_id,"recipient":p.get("recipient")})
        data=result.get("data") or result
        if result.get("status")=="error" or not data.get("challenge_id"): raise QRServiceError("challenge_service_unavailable","Authentication challenge could not be started.",503)
        with self.sessions() as s: current=s.get(QRResolution,r.resolution_id); current.challenge_id=str(data["challenge_id"]); s.commit()
        return str(data["challenge_id"])

    def _verify_challenge(self,c,r,p):
        app=os.getenv("QR_CHALLENGE_APP","identity_service")
        if load_manifest(app) is None: raise QRServiceError("challenge_service_unavailable","Authentication challenge service is not configured.",503)
        result=call_app(c,app,"verify_challenge_v1",{"challenge_id":r.challenge_id,"verification_input":p.get("verification_input"),"correlation_id":r.correlation_id}); return result.get("data") or result

    def _record(self,tenant,qr_id):
        with self.sessions() as s: row=s.execute(select(QRRecord).where(QRRecord.tenant_id==tenant,QRRecord.qr_id==qr_id)).scalar_one_or_none()
        if not row: raise QRServiceError("qr_not_found","QR record was not found.",404)
        return row

    def _resolution_by_token(self,token):
        with self.sessions() as s:r=s.execute(select(QRResolution).where(QRResolution.resolution_token_hash==token_hash(token))).scalar_one_or_none()
        if not r: raise QRServiceError("resolution_unavailable","Resolution is invalid or expired.",404)
        return r

    def _authorize_record(self,c,row,operation):
        if _admin(c):return
        caller=_caller(c); perm=self._permission(row.tenant_id,caller)
        if row.owner_app!=caller or not perm or not perm.enabled or operation not in set(perm.operations_json or []): raise QRServiceError("permission_denied","Operation is not permitted.",403)

    def _authorize_resolution(self,c,r,consume):
        if r.tenant_id!=_tenant(c): raise QRServiceError("resolution_unavailable","Resolution is invalid or expired.",404)
        caller=_caller(c)
        if caller!=r.owner_app and not _admin(c): raise QRServiceError("permission_denied","Resolution belongs to another app.",403)
        if consume and not _admin(c):
            permission=self._permission(r.tenant_id,caller)
            if not permission or "consume" not in set(permission.operations_json or []):raise QRServiceError("permission_denied","Caller may not consume resolutions.",403)

    def _permission(self,tenant,caller):
        with self.sessions() as s:return s.execute(select(QRAppPermission).where(QRAppPermission.tenant_id==tenant,QRAppPermission.requesting_app==caller)).scalar_one_or_none()

    def _data(self,r,detail=False):
        data={"qr_id":r.qr_id,"display_name":r.display_name,"owner_app":r.owner_app,"resource_type":r.resource_type,"action_key":r.action_key,"kind":r.qr_kind,"status":r.status,"token_prefix":r.public_token_prefix,"scan_count":r.scan_count,"successful_use_count":r.successful_use_count,"created_at":r.created_at.isoformat()+"Z","expires_at":r.expires_at.isoformat()+"Z" if r.expires_at else None}
        if detail:data|={"description":r.description,"resource_reference":r.resource_reference,"policy":r.policy_json,"metadata":r.metadata_json,"replaced_by_qr_id":r.replaced_by_qr_id,"replaces_qr_id":r.replaces_qr_id}
        return data

    def _text(self,p,key,maxlen,required=False):
        value=html.escape(str(p.get(key) or "").strip(),quote=False)
        if required and not value:raise QRServiceError("invalid_input",f"{key} is required.")
        if len(value)>maxlen:raise QRServiceError("invalid_input",f"{key} is too long.")
        return value or None

    def _require_key(self,p):
        key=str(p.get("idempotency_key") or "")
        if not 8<=len(key)<=128: raise QRServiceError("idempotency_key_required","idempotency_key must contain 8 to 128 characters.")

    def _request_hash(self,p):return hashlib.sha256(json.dumps(p,sort_keys=True,separators=(",",":"),default=str).encode()).hexdigest()
    def _replay(self,t,a,o,p):
        key=str(p.get("idempotency_key") or "");
        with self.sessions() as s:r=s.execute(select(QRIdempotency).where(QRIdempotency.tenant_id==t,QRIdempotency.requesting_app==a,QRIdempotency.operation==o,QRIdempotency.idempotency_key==key)).scalar_one_or_none()
        if r and not secrets.compare_digest(r.request_hash,self._request_hash(p)):raise QRServiceError("idempotency_conflict","Idempotency key was used with different input.",409)
        return dict(r.response_json) if r else None
    def _save_idempotency(self,t,a,o,p,response):
        with self.sessions() as s:
            s.add(QRIdempotency(tenant_id=t,requesting_app=a,operation=o,idempotency_key=str(p["idempotency_key"]),request_hash=self._request_hash(p),response_json=response))
            try:s.commit()
            except IntegrityError:s.rollback()
    def _audit(self,c,event,row,resolution_id=None):
        safe={"owner_app":row.owner_app,"resource_type":row.resource_type,"action_key":row.action_key,"status":row.status}
        with self.sessions() as s:s.add(QRAuditEvent(tenant_id=row.tenant_id,qr_id=row.qr_id,resolution_id=resolution_id,event=event,requesting_app=str(c.get("requesting_app") or "public"),result="success",correlation_id=str(c.get("request_id") or ""),details_json=safe));s.commit()
        AuditLogger().log(tenant_id=row.tenant_id,actor=str(c.get("requesting_app") or "public"),action=event,qr_id=row.qr_id,status="success")
    def _rate(self,c,metric,token_key=""):
        if metric=="public_resolution":
            ip=str(c.get("remote_addr") or "unknown"); now=time.monotonic()
            with _PUBLIC_RATE_LOCK:
                for key,limit in ((f"ip:{ip}",30),(f"token:{token_key}",60)):
                    attempts=[x for x in _PUBLIC_RATE.get(key,[]) if now-x<60]
                    if len(attempts)>=limit:raise QRServiceError("rate_limit_exceeded","Too many requests.",429)
                    attempts.append(now);_PUBLIC_RATE[key]=attempts
            return
        try:consume(tenant_id=str(((c.get("tenant") or {}).get("tenant_id")) or "public"),app_id="qr_service",metric=metric,metadata={"request_id":c.get("request_id")})
        except QuotaExceeded as exc:raise QRServiceError("rate_limit_exceeded","Too many requests.",429) from exc
