import uuid
from datetime import datetime, timezone
from threading import RLock


def _now_iso():
    return datetime.now(timezone.utc).isoformat()


def _parse_iso_datetime(value):
    text = str(value or "").strip()
    if not text:
        return None
    if text.endswith("Z"):
        text = text[:-1] + "+00:00"
    try:
        parsed = datetime.fromisoformat(text)
    except ValueError:
        return None
    if parsed.tzinfo is None:
        return parsed.replace(tzinfo=timezone.utc)
    return parsed.astimezone(timezone.utc)


class ProposalManager:
    """
    Stores and validates AI proposal artifacts.
    Supports policy-gated injection of child tasks as NEW tasks.
    """

    def __init__(self, max_child_depth=1, max_fanout_per_parent=2):
        self._lock = RLock()
        self._proposals = {}
        self._fanout_counter = {}
        self._max_child_depth = max_child_depth
        self._max_fanout_per_parent = max_fanout_per_parent

    def create(self, agent_id, run_id, task_id, chunk_id, proposal_content):
        proposal_id = f"proposal-{uuid.uuid4().hex[:10]}"
        record = {
            "proposal_id": proposal_id,
            "agent_id": agent_id,
            "run_id": run_id,
            "task_id": task_id,
            "chunk_id": chunk_id,
            "artifact": proposal_content,
            "status": "created",
            "created_at": _now_iso(),
            "validated_at": None,
            "injected_at": None,
            "validation": {
                "ok": False,
                "errors": [],
                "validation_mode": None,
                "validation_source": None,
                "validation_actor": None,
            },
            "archive": {
                "archived": False,
                "archived_at": None,
                "archive_mode": None,
                "archive_source": None,
                "archive_actor": None,
            },
            "child_task_id": None,
        }
        with self._lock:
            self._proposals[proposal_id] = record
        return record

    def validate(self, proposal_id, validation_mode="core_auto", validation_source="chain", validation_actor="system"):
        with self._lock:
            proposal = self._proposals.get(proposal_id)
            if not proposal:
                return {"ok": False, "errors": ["proposal_not_found"]}
            artifact = proposal["artifact"] or {}

            errors = []
            if artifact.get("artifact_type") not in {"task_draft", "wrapper_draft", "patch_draft"}:
                errors.append("invalid_artifact_type")
            if not artifact.get("title"):
                errors.append("missing_title")

            if artifact.get("artifact_type") == "task_draft":
                body = artifact.get("body")
                if not isinstance(body, dict):
                    errors.append("task_draft_body_must_be_object")
                else:
                    task_intent = body.get("task_intent")
                    if not isinstance(task_intent, dict):
                        errors.append("missing_task_intent")
                    else:
                        if not isinstance(task_intent.get("chunks"), list) or not task_intent.get("chunks"):
                            errors.append("task_intent_chunks_required")

                    depth = int(body.get("depth", 1))
                    if depth > self._max_child_depth:
                        errors.append("max_child_depth_exceeded")

                    parent_task_id = body.get("parent_task_id") or proposal.get("task_id")
                    fanout_key = str(parent_task_id)
                    current_fanout = self._fanout_counter.get(fanout_key, 0)
                    if current_fanout >= self._max_fanout_per_parent:
                        errors.append("max_fanout_per_parent_exceeded")

            ok = len(errors) == 0
            validated_at = _now_iso()
            proposal["validation"] = {
                "ok": ok,
                "errors": errors,
                "validation_mode": str(validation_mode or "core_auto"),
                "validation_source": str(validation_source or "chain"),
                "validation_actor": str(validation_actor or "system"),
                "validated_at": validated_at,
            }
            proposal["validated_at"] = validated_at
            proposal["status"] = "validated" if ok else "rejected"
            return proposal["validation"]

    def archive(self, proposal_id, archive_mode="operator_requested", archive_source="workbench", archive_actor="operator"):
        with self._lock:
            proposal = self._proposals.get(proposal_id)
            if not proposal:
                return None
            archived_at = _now_iso()
            proposal["status"] = "archived"
            proposal["archive"] = {
                "archived": True,
                "archived_at": archived_at,
                "archive_mode": str(archive_mode or "operator_requested"),
                "archive_source": str(archive_source or "workbench"),
                "archive_actor": str(archive_actor or "operator"),
            }
            return dict(proposal)

    def archive_older_than(
        self,
        older_than_seconds,
        statuses=None,
        archive_mode="operator_bulk",
        archive_source="workbench",
        archive_actor="operator",
    ):
        try:
            threshold_seconds = max(0, int(older_than_seconds))
        except (TypeError, ValueError):
            threshold_seconds = 0
        cutoff = datetime.now(timezone.utc).timestamp() - threshold_seconds
        allowed_statuses = {str(s).strip().lower() for s in (statuses or []) if str(s).strip()}
        archived_ids = []
        with self._lock:
            for proposal_id, proposal in self._proposals.items():
                current_status = str(proposal.get("status") or "").strip().lower()
                if current_status == "archived":
                    continue
                if allowed_statuses and current_status not in allowed_statuses:
                    continue
                ts = _parse_iso_datetime(proposal.get("injected_at") or proposal.get("validated_at") or proposal.get("created_at"))
                if not ts:
                    continue
                if ts.timestamp() > cutoff:
                    continue
                archived_at = _now_iso()
                proposal["status"] = "archived"
                proposal["archive"] = {
                    "archived": True,
                    "archived_at": archived_at,
                    "archive_mode": str(archive_mode or "operator_bulk"),
                    "archive_source": str(archive_source or "workbench"),
                    "archive_actor": str(archive_actor or "operator"),
                }
                archived_ids.append(str(proposal_id))
        return archived_ids

    def inject_validated(self, proposal_id, core):
        with self._lock:
            proposal = self._proposals.get(proposal_id)
            if not proposal:
                raise ValueError("proposal_not_found")
            if proposal["status"] != "validated":
                raise ValueError("proposal_not_validated")
            artifact = proposal["artifact"]
            if artifact.get("artifact_type") != "task_draft":
                raise ValueError("artifact_not_injectable")

            body = artifact.get("body", {})
            task_intent = body.get("task_intent")
            parent_task_id = body.get("parent_task_id") or proposal.get("task_id")

        child_task_id = core.ingest_intent(task_intent)

        with self._lock:
            fanout_key = str(parent_task_id)
            self._fanout_counter[fanout_key] = self._fanout_counter.get(fanout_key, 0) + 1
            proposal["status"] = "injected"
            proposal["injected_at"] = _now_iso()
            proposal["child_task_id"] = child_task_id
            proposal["parent_task_id"] = parent_task_id
            return dict(proposal)

    def get(self, proposal_id):
        with self._lock:
            proposal = self._proposals.get(proposal_id)
            return dict(proposal) if proposal else None

    def list(self, task_id=None, include_archived=True):
        with self._lock:
            values = list(self._proposals.values())
        if not include_archived:
            values = [v for v in values if str(v.get("status") or "").lower() != "archived"]
        if task_id is None:
            return [dict(v) for v in values]
        return [dict(v) for v in values if v.get("task_id") == task_id]

    def stats(self):
        with self._lock:
            proposals = list(self._proposals.values())
            return {
                "total": len(proposals),
                "created": sum(p["status"] == "created" for p in proposals),
                "validated": sum(p["status"] == "validated" for p in proposals),
                "rejected": sum(p["status"] == "rejected" for p in proposals),
                "injected": sum(p["status"] == "injected" for p in proposals),
                "archived": sum(p["status"] == "archived" for p in proposals),
            }
