from __future__ import annotations

import hashlib
from typing import Any, Dict, Iterable, Mapping, MutableMapping, Optional, TypedDict, cast

from grantflow.swarm.versioning import filter_versions

_VALID_SEVERITIES = {"low", "medium", "high"}
_VALID_SECTIONS = {"toc", "logframe", "general"}
_VALID_STATUSES = {"open", "acknowledged", "resolved"}


class FindingEntity(TypedDict, total=False):
    id: str
    finding_id: str
    code: str
    label: Optional[str]
    severity: str
    section: str
    status: str
    version_id: Optional[str]
    message: str
    rationale: Optional[str]
    fix_suggestion: Optional[str]
    fix_hint: Optional[str]
    source: str
    updated_at: Optional[str]
    updated_by: Optional[str]
    acknowledged_at: Optional[str]
    acknowledged_by: Optional[str]
    resolved_at: Optional[str]
    resolved_by: Optional[str]
    due_at: Optional[str]
    sla_hours: Optional[int]
    workflow_state: Optional[str]
    is_overdue: Optional[bool]
    age_hours: Optional[float]
    time_to_due_hours: Optional[float]


def finding_primary_id(item: Mapping[str, Any]) -> str:
    return str(item.get("finding_id") or item.get("id") or "").strip()


def _coerce_section(value: Any, *, message: str = "") -> str:
    section = str(value or "").strip().lower()
    if section in {"log_frame", "log-frame", "log frame", "mel", "indicator", "indicators"}:
        return "logframe"
    if section in {"theory_of_change", "theory-of-change", "theory of change"}:
        return "toc"
    if section in _VALID_SECTIONS:
        return section
    lowered = message.lower()
    if any(k in lowered for k in ("indicator", "mel", "logframe")):
        return "logframe"
    if any(k in lowered for k in ("toc", "objective", "assumption", "causal")):
        return "toc"
    return "general"


def _coerce_severity(value: Any) -> str:
    severity = str(value or "").strip().lower()
    if severity in {"critical", "fatal", "blocker"}:
        return "high"
    if severity in {"warn", "warning"}:
        return "medium"
    if severity in {"info", "informational"}:
        return "low"
    return severity if severity in _VALID_SEVERITIES else "medium"


def _coerce_status(value: Any) -> str:
    status = str(value or "open").strip().lower()
    if status in {"ack", "accepted"}:
        return "acknowledged"
    if status in {"done", "closed"}:
        return "resolved"
    if status in {"reopened", "pending"}:
        return "open"
    return status if status in _VALID_STATUSES else "open"


def finding_identity_key(item: Mapping[str, Any]) -> tuple[str, str, str, str, str]:
    return (
        str(item.get("code") or ""),
        str(item.get("section") or ""),
        str(item.get("version_id") or ""),
        str(item.get("message") or ""),
        str(item.get("source") or ""),
    )


def _generated_finding_id(
    *,
    code: str,
    section: str,
    version_id: Optional[str],
    message: str,
    source: str,
) -> str:
    seed = "|".join([code.strip().upper(), section.strip().lower(), str(version_id or "").strip(), message, source])
    digest = hashlib.sha256(seed.encode("utf-8")).hexdigest()[:12]
    return f"finding-{digest}"


def normalize_finding_item(
    item: Any,
    *,
    default_source: str = "rules",
) -> Optional[FindingEntity]:
    if isinstance(item, str):
        message = item.strip()
        if not message:
            return None
        section = _coerce_section(None, message=message)
        canonical_id = _generated_finding_id(
            code="LEGACY_UNSTRUCTURED_FINDING",
            section=section,
            version_id=None,
            message=message,
            source=default_source,
        )
        normalized: FindingEntity = {
            "id": canonical_id,
            "finding_id": canonical_id,
            "code": "LEGACY_UNSTRUCTURED_FINDING",
            "severity": "medium",
            "section": section,
            "status": "open",
            "version_id": None,
            "message": message,
            "rationale": "Converted from legacy unstructured finding text.",
            "fix_suggestion": "Rewrite finding as structured object with code/severity/section fields.",
            "fix_hint": "Rewrite finding as structured object with code/severity/section fields.",
            "source": default_source,
        }
        return normalized

    if not isinstance(item, dict):
        return None

    current = dict(item)
    message = str(current.get("message") or current.get("detail") or "").strip()
    if not message:
        return None

    section = _coerce_section(current.get("section"), message=message)
    severity = _coerce_severity(current.get("severity"))
    status = _coerce_status(current.get("status"))
    source = str(current.get("source") or "").strip() or default_source
    code = str(current.get("code") or "").strip() or "FINDING_UNSPECIFIED"
    version_id = str(current.get("version_id") or "").strip() or None
    canonical_id = finding_primary_id(current) or _generated_finding_id(
        code=code,
        section=section,
        version_id=version_id,
        message=message,
        source=source,
    )
    rationale = str(current.get("rationale") or "").strip() or None
    fix_suggestion = str(current.get("fix_suggestion") or current.get("fix_hint") or "").strip() or None

    normalized_item: Dict[str, Any] = dict(current)
    normalized_item["id"] = canonical_id
    normalized_item["finding_id"] = canonical_id
    normalized_item["code"] = code
    normalized_item["severity"] = severity
    normalized_item["section"] = section
    normalized_item["status"] = status
    normalized_item["message"] = message
    normalized_item["source"] = source
    normalized_item["rationale"] = rationale
    normalized_item["fix_suggestion"] = fix_suggestion
    normalized_item["fix_hint"] = fix_suggestion
    normalized_item["version_id"] = version_id

    if status == "open":
        normalized_item.pop("acknowledged_at", None)
        normalized_item.pop("acknowledged_by", None)
        normalized_item.pop("resolved_at", None)
        normalized_item.pop("resolved_by", None)
    elif status == "acknowledged":
        normalized_item.pop("resolved_at", None)
        normalized_item.pop("resolved_by", None)
    elif status == "resolved":
        pass
    return cast(FindingEntity, normalized_item)


def normalize_findings(
    items: Iterable[Any],
    *,
    previous_items: Optional[Iterable[Any]] = None,
    default_source: str = "rules",
) -> list[FindingEntity]:
    previous_by_key: Dict[tuple[str, str, str, str, str], FindingEntity] = {}
    for prev in previous_items or []:
        normalized_prev = normalize_finding_item(prev, default_source=default_source)
        if normalized_prev is None:
            continue
        previous_by_key[finding_identity_key(normalized_prev)] = normalized_prev

    normalized_items: list[FindingEntity] = []
    for item in items:
        normalized = normalize_finding_item(item, default_source=default_source)
        if normalized is None:
            continue
        normalized_dict: Dict[str, Any] = dict(normalized)

        prior = previous_by_key.get(finding_identity_key(normalized_dict))
        if prior:
            raw = item if isinstance(item, dict) else {}
            for key in (
                "id",
                "finding_id",
                "status",
                "updated_at",
                "updated_by",
                "acknowledged_at",
                "acknowledged_by",
                "resolved_at",
                "resolved_by",
                "due_at",
                "sla_hours",
                "workflow_state",
                "is_overdue",
                "age_hours",
                "time_to_due_hours",
            ):
                raw_has_value = isinstance(raw, dict) and raw.get(key) not in (None, "")
                if not raw_has_value and prior.get(key) is not None:
                    normalized_dict[key] = prior.get(key)
            canonical_id = finding_primary_id(normalized_dict)
            if canonical_id:
                normalized_dict["id"] = canonical_id
                normalized_dict["finding_id"] = canonical_id

        normalized_items.append(cast(FindingEntity, normalized_dict))
    return normalized_items


def canonicalize_findings(
    items: Iterable[Any],
    *,
    state: Optional[Mapping[str, Any]] = None,
    previous_items: Optional[Iterable[Any]] = None,
    default_source: str = "rules",
    dedupe: bool = True,
) -> list[FindingEntity]:
    normalized = normalize_findings(
        items,
        previous_items=previous_items,
        default_source=default_source,
    )
    bound = bind_findings_to_latest_versions(normalized, state=state)
    if not dedupe:
        return bound
    deduped: list[FindingEntity] = []
    by_id: Dict[str, int] = {}
    for row in bound:
        current = dict(row)
        fid = finding_primary_id(current)
        if not fid:
            deduped.append(cast(FindingEntity, current))
            continue
        current["id"] = fid
        current["finding_id"] = fid
        if fid in by_id:
            deduped[by_id[fid]] = cast(FindingEntity, current)
            continue
        by_id[fid] = len(deduped)
        deduped.append(cast(FindingEntity, current))
    return deduped


def finding_messages(findings: Iterable[Any], *, fallback: Optional[str] = None) -> list[str]:
    out: list[str] = []
    seen: set[str] = set()
    for item in findings:
        message = str((item or {}).get("message") if isinstance(item, dict) else "").strip()
        if not message:
            continue
        if message in seen:
            continue
        seen.add(message)
        out.append(message)
    if out:
        return out
    if fallback:
        return [fallback]
    return []


def state_critic_findings(
    state: Mapping[str, Any],
    *,
    previous_items: Optional[Iterable[Any]] = None,
    default_source: str = "rules",
) -> list[FindingEntity]:
    if not isinstance(state, Mapping):
        return []
    raw_critic_notes = state.get("critic_notes")
    critic_notes = raw_critic_notes if isinstance(raw_critic_notes, dict) else {}
    raw = critic_notes.get("fatal_flaws")
    if not isinstance(raw, list):
        raw = state.get("critic_fatal_flaws")
    if not isinstance(raw, list):
        return []
    return canonicalize_findings(
        raw,
        state=state,
        previous_items=previous_items,
        default_source=default_source,
        dedupe=True,
    )


def write_state_critic_findings(
    state: MutableMapping[str, Any],
    findings: Iterable[Any],
    *,
    previous_items: Optional[Iterable[Any]] = None,
    default_source: str = "rules",
) -> list[FindingEntity]:
    canonical = canonicalize_findings(
        findings,
        state=state,
        previous_items=previous_items,
        default_source=default_source,
        dedupe=True,
    )
    notes = state.get("critic_notes")
    notes_dict = dict(notes) if isinstance(notes, dict) else {}
    notes_dict["fatal_flaws"] = canonical
    state["critic_notes"] = notes_dict
    state["critic_fatal_flaws"] = canonical
    return canonical


def latest_version_id_by_section(state: Optional[Mapping[str, Any]]) -> Dict[str, str]:
    if not isinstance(state, Mapping):
        return {}
    raw_versions = state.get("draft_versions")
    if not isinstance(raw_versions, list):
        return {}
    safe_versions = [row for row in raw_versions if isinstance(row, dict)]
    section_map: Dict[str, str] = {}
    for section in ("toc", "logframe"):
        versions = filter_versions(safe_versions, section=section)
        if not versions:
            continue
        version_id = str(versions[-1].get("version_id") or "").strip()
        if version_id:
            section_map[section] = version_id
    return section_map


def bind_findings_to_latest_versions(
    findings: Iterable[Any],
    *,
    state: Optional[Mapping[str, Any]] = None,
) -> list[FindingEntity]:
    section_versions = latest_version_id_by_section(state)
    out: list[FindingEntity] = []
    for finding in findings:
        if not isinstance(finding, dict):
            continue
        current = dict(finding)
        section = str(current.get("section") or "").strip().lower()
        if section in section_versions and not str(current.get("version_id") or "").strip():
            current["version_id"] = section_versions[section]
        out.append(cast(FindingEntity, current))
    return out
