"""Bounded, benchmark-clean Stage Q provider context construction."""

from __future__ import annotations

import json
import re
import hashlib
from uuid import uuid4

from apk_workspace.models import AppWorkspace
from apk_workspace.search import WorkspaceSearch
from .capabilities import derive_trusted_execution_scope
from discovery.context import THREAT_MODEL, sanitized_application

from .models import (
    CapabilityView,
    ValidationAssessmentAction,
    ValidationBudgets,
    ValidationContext,
    ValidationObservation,
)


class ValidationContextError(ValueError):
    pass


def canonical_json(value: object) -> str:
    return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)


def serialized_bytes(value: object) -> int:
    return len(canonical_json(value).encode("utf-8"))


def model_visible_finding(finding) -> dict[str, object]:
    payload = finding.to_dict()
    speculative = dict(payload["speculative_finding"])
    speculative["finding_id"] = "TARGET-FINDING"
    payload["speculative_finding"] = speculative
    return payload


def _compact(observation: ValidationObservation) -> ValidationObservation:
    if serialized_bytes(observation.to_dict()) <= 6_144:
        return observation
    excerpt = canonical_json(observation.to_dict()["data"]).encode("utf-8")[:4_096]
    return ValidationObservation(
        observation_id=observation.observation_id,
        source=observation.source,
        originating_tool=observation.originating_tool,
        success=observation.success,
        execution_status=observation.execution_status,
        summary=observation.summary,
        data={"bounded_serialized_excerpt": excerpt.decode("utf-8", errors="ignore")},
        evidence_refs=observation.evidence_refs,
        truncated=True,
        limitations=tuple(
            dict.fromkeys(
                (*observation.limitations, "Observation data was compacted for context.")
            )
        ),
        failure_reason=observation.failure_reason,
    )


class ValidationContextBuilder:
    def __init__(self, maximum_bytes: int = 24_576) -> None:
        if type(maximum_bytes) is not int or maximum_bytes <= 0:
            raise ValueError("Context byte limit must be a positive integer.")
        self.maximum_bytes = maximum_bytes
        self.validation_run_id = uuid4().hex

    def build(
        self,
        *,
        workspace: AppWorkspace,
        finding,
        supporting_evidence: tuple[ValidationObservation, ...],
        observations: tuple[ValidationObservation, ...],
        assessments: tuple[ValidationAssessmentAction, ...],
        capability_view: CapabilityView,
        budgets: ValidationBudgets,
        provider_turns: int,
        weighted_cost: int,
        mutations: int,
        ui_actions: int,
        activity_experiment=None,
        validation_run_id: str | None = None,
    ) -> ValidationContext:
        application = sanitized_application(workspace).to_dict()
        scope = derive_trusted_execution_scope(workspace)
        paths = [record.relative_path for record in WorkspaceSearch(workspace).resources() if record.text_readable]
        terms = set(re.findall(r"[A-Za-z_][A-Za-z_0-9]*", canonical_json(finding.to_dict()).casefold()))
        paths.sort(key=lambda path: (path != workspace.manifest_path, not path.endswith("/values/strings.xml"), -sum(term in path.casefold() for term in terms if len(term) > 3), path))
        guidance = {
            "manifest_path": workspace.manifest_path,
            "indexed_text_resource_paths": paths[:16],
            "resource_paths_truncated": len(paths) > 16,
            "declared_provider_authorities": list(scope.get("declared_provider_authorities", ()))[:16],
            "approved_intent_routes": [{"target_type": route.target_type, "action": route.action, "categories": list(route.categories), "data_schemes": list(route.data_schemes)} for route in scope.get("approved_intent_routes", ())][:16],
        }
        target = model_visible_finding(finding)
        assessment_payloads = tuple(item.to_dict() for item in assessments[-3:])
        progress = None
        if activity_experiment is not None:
            from .activity_strategy import activity_progress
            # The orchestrator's per-run accepted log is durable; the retained
            # provider window is only a bounded evidentiary view of that log.
            progress = activity_progress(activity_experiment, observations)
            progress.update({
                "run_id": validation_run_id or self.validation_run_id,
                "finding_sha256": hashlib.sha256(canonical_json(finding.to_dict()).encode()).hexdigest(),
            })

        def make(
            support: tuple[ValidationObservation, ...],
            selected: tuple[ValidationObservation, ...],
        ) -> ValidationContext:
            return ValidationContext(
                workspace_guidance=guidance,
                activity_experiment=None if activity_experiment is None else activity_experiment.to_dict(),
                activity_progress=progress,
                threat_model=THREAT_MODEL,
                application=application,
                target_finding=target,
                supporting_evidence=support,
                observations=selected,
                assessments=assessment_payloads,
                capability_view=capability_view,
                remaining_provider_turns=max(0, budgets.max_provider_turns - provider_turns),
                remaining_weighted_tool_cost=max(0, budgets.max_weighted_tool_cost - weighted_cost),
                remaining_mutations=max(0, budgets.max_device_mutations - mutations),
                remaining_ui_actions=max(0, budgets.max_ui_actions - ui_actions),
            )

        compact_support = tuple(_compact(item) for item in supporting_evidence)
        compact_observations = tuple(_compact(item) for item in observations)
        context = make(compact_support, ())
        if serialized_bytes(context.to_dict()) > self.maximum_bytes:
            # Supporting evidence remains referenced in the trace, but only the most
            # recent compact records fit in the provider view.
            retained: list[ValidationObservation] = []
            for item in reversed(compact_support):
                proposal = tuple(reversed([*retained, item]))
                if serialized_bytes(make(proposal, ()).to_dict()) <= self.maximum_bytes:
                    retained.append(item)
            compact_support = tuple(reversed(retained))
            context = make(compact_support, ())
        if serialized_bytes(context.to_dict()) > self.maximum_bytes:
            raise ValidationContextError("Trusted Stage Q context base exceeds its limit.")
        selected: list[ValidationObservation] = []
        for item in reversed(compact_observations):
            proposal = tuple(reversed([*selected, item]))
            candidate = make(compact_support, proposal)
            if serialized_bytes(candidate.to_dict()) <= self.maximum_bytes:
                selected.append(item)
        context = make(compact_support, tuple(reversed(selected)))
        if serialized_bytes(context.to_dict()) > self.maximum_bytes:
            raise ValidationContextError("Stage Q context exceeds its hard limit.")
        return context
