from __future__ import annotations

import io
import json
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, Mapping, Optional

import pandas as pd

from config.paths import DATA_ROOT
from receipts.writer import write_receipt
from receipts.partitioning import partition_slug, normalise_token
from receipts.storage import read_bytes, read_text
from scripts.receipt_sign import verify_receipt as _verify_signed_payload
from receipts.index import lookup_receipt

__all__ = [
    "DATA_ROOT",
    "write_receipt",
    "load_recent_receipts",
    "fetch_by_id",
    "verify_signature",
]


def load_recent_receipts(
    domain: Optional[str] = None,
    days: int = 30,
    tenant: Optional[str] = None,
) -> pd.DataFrame:
    """Load recent receipts from the ledger Parquet store."""

    base_path = Path(DATA_ROOT) / "receipts" / "ledger"
    if not base_path.exists():
        return pd.DataFrame()

    cutoff_date = datetime.utcnow().date() - timedelta(days=max(int(days), 0))
    frames: list[pd.DataFrame] = []

    tenant_filter = tenant or os.getenv("LOOM_TENANT")
    tenant_slug = partition_slug(tenant_filter) if tenant_filter else None
    domain_slug = partition_slug(domain) if domain else None

    for tenant_dir in sorted(p for p in base_path.iterdir() if p.is_dir()):
        if tenant_slug and tenant_dir.name != tenant_slug:
            continue
        for domain_dir in sorted(p for p in tenant_dir.iterdir() if p.is_dir()):
            if domain_slug and domain_dir.name != domain_slug:
                continue
            for partition in sorted(domain_dir.glob("DATE=*")):
                token = partition.name.split("=", 1)[-1]
                try:
                    partition_date = datetime.strptime(token, "%Y-%m-%d").date()
                except ValueError:
                    continue
                if partition_date < cutoff_date:
                    continue

                for part_file in partition.glob("*.parquet"):
                    try:
                        raw = read_bytes(part_file, tenant_hint=tenant_dir.name)
                        frame = pd.read_parquet(io.BytesIO(raw))
                    except Exception:
                        continue
                    frames.append(frame)

    if not frames:
        return pd.DataFrame()

    combined = pd.concat(frames, ignore_index=True, sort=False)
    tenant_lower = normalise_token(tenant_filter).lower() if tenant_filter else None
    if domain:
        domain_lower = str(domain).lower()
        if "domain" in combined.columns:
            combined = combined[combined["domain"].astype(str).str.lower() == domain_lower]
        elif "tenant" in combined.columns:
            combined = combined[combined["tenant"].astype(str).str.lower() == domain_lower]
    if tenant_lower and "tenant" in combined.columns:
        combined = combined[combined["tenant"].astype(str).str.lower() == tenant_lower]

    if "council_disagreement_std_norm" not in combined.columns:
        if "council_disagreement_std" in combined.columns or "council_std" in combined.columns:
            if "council_disagreement_std" in combined.columns:
                norm_series = combined["council_disagreement_std"]
            else:
                norm_series = pd.Series([None] * len(combined), index=combined.index)
            if "council_std" in combined.columns:
                norm_series = norm_series.fillna(combined["council_std"])
            combined["council_disagreement_std_norm"] = norm_series
        elif "metrics" in combined.columns:
            def _coalesce_from_metrics(payload: Any) -> Optional[float]:
                if isinstance(payload, Mapping):
                    for alias in ("council_disagreement_std", "council_std", "council_disagreement"):
                        value = payload.get(alias)
                        if value is None:
                            continue
                        try:
                            return float(value)
                        except (TypeError, ValueError):
                            continue
                return None

            combined["council_disagreement_std_norm"] = combined["metrics"].apply(_coalesce_from_metrics)

    return combined.reset_index(drop=True)


def fetch_by_id(receipt_id: str, *, root: Optional[Path] = None) -> Dict[str, Any]:
    """Fetch a canonical receipt payload by id from the inbox."""

    base_path = Path(root or DATA_ROOT) / "receipts" / "inbox"
    if not base_path.exists():
        raise FileNotFoundError(f"receipt inbox not found: {base_path}")

    entry = lookup_receipt(receipt_id, root=root)
    if entry is not None:
        target = entry.path
    else:
        candidates = list(base_path.rglob(f"{receipt_id}.json"))
        if not candidates:
            raise FileNotFoundError(f"receipt '{receipt_id}' not found")
        target = max(candidates, key=lambda path: path.stat().st_mtime)
    try:
        rel_parts = target.relative_to(base_path).parts
    except ValueError:
        rel_parts = ()
    tenant_hint = rel_parts[0] if rel_parts else None
    payload = read_text(target, tenant_hint=tenant_hint)
    return json.loads(payload)


def verify_signature(receipt: Mapping[str, Any]) -> Dict[str, Any]:
    """Verify the signature block for a receipt payload."""

    signatures = receipt.get("signatures") or []
    signature_info: Dict[str, Any]
    if isinstance(signatures, list) and signatures:
        signature_info = dict(signatures[0])
    else:
        signature_info = dict(receipt.get("signature") or {})

    algo = (
        signature_info.get("algorithm")
        or signature_info.get("alg")
        or signature_info.get("algo")
    )
    kid = signature_info.get("kid") or signature_info.get("key_id")
    sig_b64 = (
        signature_info.get("signature_b64")
        or signature_info.get("sig")
        or signature_info.get("sig_b64")
        or signature_info.get("signature")
    )
    pubkey = (
        signature_info.get("public_key")
        or receipt.get("signing_public_key")
        or os.getenv("RECEIPTS_PUBKEY_B64")
    )

    if not sig_b64 or not pubkey:
        return {"algo": algo, "key_id": kid, "verified": False}

    payload = dict(receipt)
    # Normalise signature block to legacy format expected by verifier.
    normalised_sig = {
        "alg": (str(algo).upper() if isinstance(algo, str) else "ED25519"),
        "kid": kid,
        "sig": sig_b64,
    }
    payload["signature"] = normalised_sig

    try:
        verified = _verify_signed_payload(payload, pubkey)
    except Exception:
        verified = False

    return {"algo": algo, "key_id": kid, "verified": bool(verified)}
