from collections import defaultdict
from datetime import datetime, timedelta, timezone

from pymongo import ASCENDING
from pymongo.errors import CollectionInvalid

from Analytic.centro_ids import normalize_area_id


SCAPP_DATABASE_NAME = "ScappDB"
FEEDER_SCORE_COLLECTION_NAME = "feeder_score"
VALID_SCOPES = {"jaula", "centro"}
SUPPORTED_RW_CONTRACT_VERSIONS = frozenset({1})
LEGACY_RW_CONTRACT_VERSION = 1
LEGACY_RW_CONTRACT_V1_ALGO_VERSIONS = frozenset({
    "feeder_score_reason_audit_v1",
    "feeder_score_reason_audit_v2",
})
MIXED_ALGO_VERSION = "__mixed__"
MIXED_CONFIG_VERSION = "__mixed__"


def _feeder_score_logger(text):
    print(text)


def _capitalize_token(value):
    return str(value).strip().capitalize()


def _coerce_timestamp(value):
    if isinstance(value, datetime):
        timestamp = value
    elif isinstance(value, str):
        raw_value = value.strip()
        if raw_value.endswith("Z"):
            raw_value = raw_value[:-1] + "+00:00"
        try:
            timestamp = datetime.fromisoformat(raw_value)
        except ValueError as exc:
            raise ValueError("timestamp invalido") from exc
    else:
        raise ValueError("timestamp invalido")

    if timestamp.tzinfo is not None:
        timestamp = timestamp.astimezone(timezone.utc).replace(tzinfo=None)
    return timestamp


def ensure_feeder_score_collection(scapp_db, database_name=FEEDER_SCORE_COLLECTION_NAME):
    if database_name not in scapp_db.list_collection_names():
        try:
            scapp_db.create_collection(database_name)
            _feeder_score_logger(f"Coleccion {database_name} creada como coleccion normal.")
        except CollectionInvalid:
            pass

    collection = scapp_db[database_name]
    collection.create_index(
        [("centro_id", ASCENDING), ("camera_id", ASCENDING), ("timestamp", ASCENDING)],
        unique=True,
        name="idx_feeder_score_centro_camera_ts",
        background=True,
    )
    collection.create_index(
        [("centro_id", ASCENDING), ("timestamp", ASCENDING)],
        name="idx_feeder_score_centro_ts",
        background=True,
    )
    collection.create_index(
        [("scope", ASCENDING), ("centro_id", ASCENDING), ("timestamp", ASCENDING)],
        name="idx_feeder_score_scope_centro_ts",
        background=True,
    )
    return collection


def validate_and_normalize_feeder_score_payload(payload):
    docs = [payload] if isinstance(payload, dict) else payload
    if not isinstance(docs, list):
        raise ValueError("Payload debe ser dict o lista de dicts")

    normalized = []
    for doc in docs:
        if not isinstance(doc, dict):
            raise ValueError(f"Documento invalido: {doc!r}")

        camera_id = doc.get("camera_id")
        if not camera_id:
            raise ValueError("Falta 'camera_id' en alguno de los documentos")

        scope = doc.get("scope", "jaula")
        if scope not in VALID_SCOPES:
            raise ValueError(f"scope invalido: {scope!r}")

        normalized.append(
            {
                **{key: value for key, value in doc.items() if key != "_id"},
                "camera_id": camera_id,
                "timestamp": _coerce_timestamp(doc.get("timestamp")),
                "scope": scope,
                "data": doc.get("data") or {},
            }
        )
    return normalized


def save_feeder_score_payload(scapp_db, docs, centro_id):
    collection = ensure_feeder_score_collection(scapp_db)
    updated_at = datetime.now()
    saved = 0
    for doc in docs or []:
        collection.replace_one(
            {
                "centro_id": centro_id,
                "camera_id": doc["camera_id"],
                "timestamp": doc["timestamp"],
            },
            {
                **doc,
                "centro_id": centro_id,
                "updated": updated_at,
            },
            upsert=True,
        )
        saved += 1

    _feeder_score_logger(f"FeederScore: {saved} documentos almacenados/actualizados para {centro_id}.")
    return saved


def fetch_feeder_score_data(collection, centro_id, start_day, end_day):
    try:
        cursor = collection.find(
            {
                "centro_id": centro_id,
                "timestamp": {"$gte": start_day, "$lt": end_day},
            },
            {"_id": 0},
        ).sort([("timestamp", ASCENDING), ("camera_id", ASCENDING)])
    except Exception as exc:
        _feeder_score_logger(f"Error leyendo feeder score ({exc.__traceback__.tb_lineno}): {exc}")
        return {}

    grouped = defaultdict(dict)
    for doc in cursor:
        timestamp = doc.get("timestamp")
        if not isinstance(timestamp, datetime):
            continue
        grouped[timestamp.strftime("%d/%m/%Y")][doc.get("camera_id")] = {
            "scope": doc.get("scope", "jaula"),
            **(doc.get("data") or {}),
        }
    return dict(sorted(grouped.items()))


def get_last_feeder_score_received_at(scapp_db, centro_id):
    if FEEDER_SCORE_COLLECTION_NAME not in scapp_db.list_collection_names():
        return None

    doc = scapp_db[FEEDER_SCORE_COLLECTION_NAME].find_one(
        {"centro_id": centro_id},
        {"_id": 0, "updated": 1, "timestamp": 1},
        sort=[("updated", -1), ("timestamp", -1)],
    )
    if not doc:
        return None
    return doc.get("updated") or doc.get("timestamp")


def compute_feeder_score_by_area(scapp_db, area_id, start_day, day_ranges):
    area_id = normalize_area_id(area_id)
    end_day = start_day + timedelta(days=int(day_ranges))
    grouped_docs = _fetch_docs_by_day_and_center(
        scapp_db,
        {"centro_id": {"$regex": f"^{area_id}-"}, "timestamp": {"$gte": start_day, "$lt": end_day}},
    )

    response = defaultdict(dict)
    for day, centers in grouped_docs.items():
        for centro_id, docs in centers.items():
            cell = _build_center_rollup_cell(centro_id, docs)
            if cell:
                response[day][centro_id] = cell

    return {day: dict(sorted(items.items())) for day, items in sorted(response.items())}


def compute_feeder_score_by_region(scapp_db, region_id, start_day, day_ranges, granularity=None):
    region_id = _capitalize_token(region_id)
    end_day = start_day + timedelta(days=int(day_ranges))
    grouped_docs = _fetch_docs_by_day_and_center(
        scapp_db,
        {"centro_id": {"$regex": f"^{region_id}-"}, "timestamp": {"$gte": start_day, "$lt": end_day}},
    )

    response = defaultdict(dict)
    for day, centers in grouped_docs.items():
        area_cells = defaultdict(list)
        for centro_id, docs in centers.items():
            center_cell = _build_center_rollup_cell(centro_id, docs)
            if not center_cell:
                continue
            parts = centro_id.split("-")
            area_id = "-".join(parts[:2]) if len(parts) >= 2 else region_id
            area_cells[area_id].append(center_cell)

        if granularity == "center":
            for centers_in_area in area_cells.values():
                for center_cell in centers_in_area:
                    response[day][center_cell["name"]] = center_cell
        else:
            for area_id, centers_in_area in area_cells.items():
                response[day][area_id] = _build_area_rollup_cell(area_id, centers_in_area)

    return {day: dict(sorted(items.items())) for day, items in sorted(response.items())}


def _fetch_docs_by_day_and_center(scapp_db, match_filter):
    if FEEDER_SCORE_COLLECTION_NAME not in scapp_db.list_collection_names():
        return {}

    cursor = scapp_db[FEEDER_SCORE_COLLECTION_NAME].find(match_filter, {"_id": 0}).sort([
        ("timestamp", ASCENDING),
        ("centro_id", ASCENDING),
        ("scope", ASCENDING),
        ("camera_id", ASCENDING),
    ])

    grouped = defaultdict(lambda: defaultdict(list))
    for doc in cursor:
        timestamp = doc.get("timestamp")
        centro_id = doc.get("centro_id")
        if not isinstance(timestamp, datetime) or not centro_id:
            continue
        grouped[timestamp.strftime("%d/%m/%Y")][centro_id].append(doc)
    return grouped


def _build_center_rollup_cell(centro_id, docs):
    center_doc = next((doc for doc in docs if doc.get("scope") == "centro" or doc.get("camera_id") == "__CENTER__"), None)
    cage_docs = [doc for doc in docs if doc is not center_doc and doc.get("camera_id") != "__CENTER__"]
    if center_doc is None:
        return None

    center_data = center_doc.get("data") or {}
    food = 0.0
    alarm_total = 0
    alarm_unattended = 0
    reaction_samples = []
    scored_cages = set()

    for doc in cage_docs:
        data = doc.get("data") or {}
        food += _number(data.get("delivered_food_kg"), 0.0)
        alarm_total += int(_number(_alarm_value(data, "alarmas_total"), 0))
        alarm_unattended += int(_number(_alarm_value(data, "alarmas_no_atendidas"), 0))
        reaction_samples.extend(_reaction_samples(data.get("reaction_time")))
        if data.get("score") is not None:
            scored_cages.add(doc.get("camera_id"))

    cell = {
        "scope": "centro",
        "name": centro_id,
        "status": center_data.get("status"),
        "score": center_data.get("score"),
        "aggregation": center_data.get("aggregation", "food_weighted"),
        "delivered_food_kg": round(food, 6),
        "alarmas_total": alarm_total,
        "alarmas_no_atendidas": alarm_unattended,
        "reaction_time": _summarize_reaction(reaction_samples),
        "n_jaulas_scored": center_data.get("n_jaulas_scored", len(scored_cages)),
        "n_jaulas_fasted": center_data.get("n_jaulas_fasted"),
    }
    if isinstance(center_data.get("rw"), dict):
        cell["rw"] = center_data.get("rw")
    return cell


def _build_area_rollup_cell(area_id, center_cells):
    food = sum(max(0.0, _number(cell.get("delivered_food_kg"), 0.0)) for cell in center_cells)
    score_num = 0.0
    score_den = 0.0
    alarm_total = 0
    alarm_unattended = 0
    reaction_samples = []
    scored_centers = 0

    for cell in center_cells:
        score = _number(cell.get("score"))
        weight = max(0.0, _number(cell.get("delivered_food_kg"), 0.0))
        if score is not None:
            scored_centers += 1
            if weight > 0:
                score_num += score * weight
                score_den += weight
        alarm_total += int(_number(cell.get("alarmas_total"), 0))
        alarm_unattended += int(_number(cell.get("alarmas_no_atendidas"), 0))
        reaction_samples.extend(_reaction_samples(cell.get("reaction_time")))

    return {
        "scope": "area",
        "name": area_id,
        "status": "final" if scored_centers else "fasted",
        "score": round(score_num / score_den, 2) if score_den > 0 else None,
        "aggregation": "food_weighted",
        "delivered_food_kg": round(food, 6),
        "alarmas_total": alarm_total,
        "alarmas_no_atendidas": alarm_unattended,
        "reaction_time": _summarize_reaction(reaction_samples),
        "n_centros_scored": scored_centers,
        "rw": aggregate_rw((cell.get("rw") for cell in center_cells), "food_weighted"),
    }


def aggregate_rw(kernels, aggregation="food_weighted"):
    # Espejo de §4 de feeder_score_reponderacion.md; actualizar junto con on-prem/frontends.
    children = [
        kernel for kernel in kernels or []
        if isinstance(kernel, dict) and isinstance(kernel.get("dims"), dict) and kernel.get("dims")
    ]
    if not children:
        return None

    weighted = aggregation == "food_weighted"
    total_weight = 0.0
    food_total = 0.0
    deliv_num = 0.0
    algo_versions = set()
    contract_versions = set()
    config_versions = set()
    bases = set()
    dim_keys = set()

    for kernel in children:
        food = max(0.0, _number(kernel.get("food_kg"), 0.0))
        weight = food if weighted else 1.0
        if weight <= 0:
            weight = 1.0
        total_weight += weight
        food_total += food
        deliv_num += weight * _number(kernel.get("deliv_pct"), 0.0)
        algo_versions.update(_kernel_algo_versions(kernel))
        contract_versions.add(_rw_contract_version(kernel))
        if kernel.get("config_version"):
            config_versions.add(kernel.get("config_version"))
        bases.add(_number(kernel.get("base"), 0.0))
        dim_keys.update(kernel.get("dims", {}).keys())

    if total_weight <= 0 or len(contract_versions) != 1:
        return None
    contract_version = next(iter(contract_versions))
    if contract_version not in SUPPORTED_RW_CONTRACT_VERSIONS:
        return None

    dims = {}
    for dim in sorted(dim_keys):
        cov_num = 0.0
        q_num = 0.0
        q_den = 0.0
        for kernel in children:
            item = (kernel.get("dims") or {}).get(dim) or {}
            food = max(0.0, _number(kernel.get("food_kg"), 0.0))
            weight = food if weighted else 1.0
            if weight <= 0:
                weight = 1.0
            cov = max(0.0, min(1.0, _number(item.get("cov"), 0.0)))
            q = max(0.0, min(1.0, _number(item.get("q"), 0.0)))
            cov_num += weight * cov
            q_num += weight * cov * q
            q_den += weight * cov
        dims[dim] = {
            "q": round(q_num / q_den, 6) if q_den > 0 else 0.0,
            "cov": round(cov_num / total_weight, 6) if total_weight > 0 else 0.0,
        }

    sorted_algo_versions = sorted(algo_versions)
    return {
        "algo_version": sorted_algo_versions[0] if len(sorted_algo_versions) == 1 else MIXED_ALGO_VERSION,
        "algo_versions": sorted_algo_versions,
        "contract_version": contract_version,
        "config_version": next(iter(config_versions)) if len(config_versions) == 1 else MIXED_CONFIG_VERSION,
        "base": next(iter(bases)) if len(bases) == 1 else None,
        "food_kg": round(food_total if weighted else total_weight, 6),
        "deliv_pct": round(deliv_num / total_weight, 6),
        "dims": dims,
    }


def reweight(kernel, params):
    if not is_recomputable(kernel):
        return None
    weights = (params or {}).get("weights") or {}
    base = _number((params or {}).get("base"), _number(kernel.get("base"), 0.0))
    sfr_cap = _number((params or {}).get("sfrCap"), 15.0)
    sfr_per_pct = _number((params or {}).get("sfrPerPct"), 0.2)
    allow_overscore = bool((params or {}).get("allowOverscore", True))
    num = 0.0
    den = 0.0
    for dim, item in (kernel.get("dims") or {}).items():
        weight = max(0.0, _number(weights.get(dim), 0.0))
        cov = max(0.0, min(1.0, _number(item.get("cov"), 0.0)))
        q = max(0.0, min(1.0, _number(item.get("q"), 0.0)))
        num += weight * cov * q
        den += weight * cov
    q_value = num / den if den > 0 else 1.0
    bonus = min(max(0.0, _number(kernel.get("deliv_pct"), 0.0) - 100.0) * sfr_per_pct, sfr_cap) * q_value
    ceiling = 100.0 + sfr_cap if allow_overscore else 100.0
    return max(base, min(ceiling, base + (100.0 - base) * q_value + bonus))


def is_recomputable(kernel):
    return (
        isinstance(kernel, dict)
        and _rw_contract_version(kernel) in SUPPORTED_RW_CONTRACT_VERSIONS
        and isinstance(kernel.get("dims"), dict)
        and bool(kernel.get("dims"))
    )


def _rw_contract_version(kernel):
    if not isinstance(kernel, dict):
        return None
    version = kernel.get("contract_version")
    if version is None and kernel.get("algo_version") in LEGACY_RW_CONTRACT_V1_ALGO_VERSIONS:
        return LEGACY_RW_CONTRACT_VERSION
    number = _number(version, None)
    return int(number) if number is not None and float(number).is_integer() else None


def _kernel_algo_versions(kernel):
    versions = kernel.get("algo_versions") if isinstance(kernel, dict) else None
    if isinstance(versions, (list, tuple, set)):
        cleaned = {str(version) for version in versions if version}
        if cleaned:
            return cleaned
    version = kernel.get("algo_version") if isinstance(kernel, dict) else None
    return {str(version)} if version else {"__unknown__"}


def _number(value, fallback=None):
    try:
        number = float(value)
    except (TypeError, ValueError):
        return fallback
    return number


def _alarm_value(data, key):
    reasons = data.get("reasons_summary") if isinstance(data, dict) else {}
    if isinstance(reasons, dict) and reasons.get(key) is not None:
        return reasons.get(key)
    return data.get(key) if isinstance(data, dict) else None


def _reaction_samples(reaction_time):
    if not isinstance(reaction_time, dict):
        return []
    samples = reaction_time.get("samples_sec")
    if isinstance(samples, list):
        return [_number(value) for value in samples if _number(value) is not None]
    avg = reaction_time.get("mean_sec", reaction_time.get("avg"))
    n = int(_number(reaction_time.get("n"), 0))
    avg_number = _number(avg)
    if avg_number is None or n <= 0:
        return []
    return [avg_number] * n


def _summarize_reaction(samples_sec):
    samples = sorted(value for value in (_number(item) for item in samples_sec or []) if value is not None)
    if not samples:
        return {"n": 0, "samples_sec": []}
    return {
        "n": len(samples),
        "mean_sec": sum(samples) / len(samples),
        "p95_sec": _percentile(samples, 95),
        "max_sec": samples[-1],
        "samples_sec": samples,
    }


def _percentile(sorted_values, pct):
    if not sorted_values:
        return None
    if len(sorted_values) == 1:
        return sorted_values[0]
    pos = (len(sorted_values) - 1) * (pct / 100)
    low = int(pos)
    high = min(low + 1, len(sorted_values) - 1)
    frac = pos - low
    return sorted_values[low] + (sorted_values[high] - sorted_values[low]) * frac
