"""Moteur unique de génération des échéances depuis les tâches de lot."""
from datetime import date, timedelta

from app_new.extensions import db
from app_new.core.models.planning import Meter, ScheduledTask
from app_new.core.models.maintenance import Intervention, LotTask
from app_new.core.services.planning_service import PlanningService


def _targets(lot, application_scope="auto", equipment_ids=None):
    """Résout la portée d'une règle sans créer une tâche par chaise par défaut."""
    result = []
    for root in lot.equipments.filter_by(is_deleted=False):
        nodes = [root] + [item for item in root.all_children if not item.is_deleted]
        scope = application_scope
        if scope == "auto":
            if root.tracked_individually:
                scope = "unit"
            else:
                scope = "group" if root.effective_management_mode in ("quantitatif", "structure", "consommable") else "unit"

        if scope in ("group", "location"):
            localized_groups = []
            for item in nodes:
                if not item.is_group or not item.effective_room:
                    continue
                if not any(child.is_group and not child.is_deleted for child in item.children):
                    localized_groups.append(item)
            result.extend(localized_groups or [root])
        else:
            leaves = [item for item in nodes if not item.is_group]
            # Un groupe quantitatif non individualisé reste une cible collective.
            result.extend(leaves or [root])

    # Une initialisation demandée depuis une fiche doit toujours pouvoir cibler
    # cette fiche, notamment une exception individualisée issue d'un groupe.
    if equipment_ids:
        from app_new.core.models.equipment import Equipment
        explicit = Equipment.query.filter(Equipment.id.in_(set(equipment_ids)), Equipment.is_deleted.is_(False)).all()
        result.extend(item for item in explicit if item.effective_lot_id == lot.id)
    return list({item.id: item for item in result}.values())


def _task_targets(task, equipment_ids=None):
    """Résout le périmètre déclaré par la règle préventive."""
    if getattr(task, "scope_mode", "dynamic") == "manual":
        targets = list(task.applicable_equipments())
        if equipment_ids is not None:
            wanted = set(equipment_ids)
            targets = [e for e in targets if e.id in wanted]
        return targets
    return _targets(task.lot, task.application_scope, equipment_ids)


def next_working_day(candidate):
    for _ in range(370):
        if PlanningService.get_working_hours(candidate):
            return candidate
        candidate += timedelta(days=1)
    raise ValueError("Aucune journée travaillée disponible dans les 12 prochains mois.")


def _last_completion(task, equipment):
    dates = []
    scheduled = ScheduledTask.query.filter(
        ScheduledTask.lot_task_id == task.id,
        ScheduledTask.equipment_id == equipment.id,
        ScheduledTask.status.in_(['completed', 'done']),
    ).order_by(ScheduledTask.completed_at.desc(), ScheduledTask.scheduled_date.desc()).first()
    if scheduled:
        if scheduled.completed_at:
            dates.append(scheduled.completed_at.date())
        elif scheduled.scheduled_date:
            dates.append(scheduled.scheduled_date)
    intervention = Intervention.query.filter(
        Intervention.lot_task_id == task.id,
        Intervention.equipment_id == equipment.id,
        Intervention.status.in_(['terminee', 'cloturee']),
        Intervention.is_deleted.is_(False),
    ).order_by(Intervention.completed_at.desc(), Intervention.completed_date.desc()).first()
    if intervention:
        if intervention.completed_at:
            dates.append(intervention.completed_at.date())
        elif intervention.completed_date:
            dates.append(intervention.completed_date)
        elif intervention.scheduled_date:
            dates.append(intervention.scheduled_date)
    return max(dates) if dates else None


def _last_completions(task):
    """Charge les dernières réalisations en deux requêtes, pas par équipement."""
    from collections import defaultdict
    result = defaultdict(list)
    scheduled = ScheduledTask.query.filter(
        ScheduledTask.lot_task_id == task.id,
        ScheduledTask.status.in_(['completed', 'done']),
    ).all()
    for item in scheduled:
        value = item.completed_at.date() if item.completed_at else item.scheduled_date
        if value and item.equipment_id:
            result[item.equipment_id].append(value)
    interventions = Intervention.query.filter(
        Intervention.lot_task_id == task.id,
        Intervention.status.in_(['terminee', 'cloturee']),
        Intervention.is_deleted.is_(False),
    ).all()
    for item in interventions:
        value = item.completed_at.date() if item.completed_at else item.completed_date or item.scheduled_date
        if value and item.equipment_id:
            result[item.equipment_id].append(value)
    return {equipment_id: max(values) for equipment_id, values in result.items()}


def _calendar_due(task, equipment, today):
    last = _last_completion(task, equipment)
    interval = task.jours_entre_interventions or 0
    return last + timedelta(days=interval) if last and interval else today


def generate_due_tasks(task: LotTask, event=None, today=None, equipment_ids=None, commit=True):
    """Génère les échéances sans doublon et retourne les objets créés."""
    today = today or date.today()
    if not task.is_active:
        return []
    # Une action conditionnelle/curative est déclenchée par un constat ou une
    # intervention, jamais par une échéance calendaire silencieuse.
    if getattr(task, "task_nature", "unknown") == "conditional_curative" and event is None:
        return []
    if task.trigger_type in ("event", "weather") and task.trigger_event != event:
        return []
    if task.trigger_type == "season":
        start, end = task.season_start_month or 1, task.season_end_month or 12
        inside = start <= today.month <= end if start <= end else today.month >= start or today.month <= end
        if not inside:
            return []
    created = []
    if not task.lot or not task.lot.is_present:
        return []
    requested_ids = set(equipment_ids) if equipment_ids is not None else None
    existing_by_equipment = {
        item.equipment_id for item in ScheduledTask.query.filter(
            ScheduledTask.lot_task_id == task.id,
            ScheduledTask.status.in_(['planned', 'in_progress', 'suspended', 'needs_duration']),
        ).all()
    }
    last_completions = _last_completions(task) if task.trigger_type == 'calendar' else {}
    for equipment in _task_targets(task, requested_ids):
        if equipment_ids is not None and equipment.id not in set(equipment_ids):
            continue
        if equipment.lifecycle_status == "jete" or equipment.status == "jete":
            continue
        unavailable = equipment.lifecycle_status in ("en_reparation", "mise_au_rebut_planifiee")
        if unavailable and not task.is_safety_critical and task.unavailable_policy == "postpone":
            continue
        if task.trigger_type == "meter":
            meter = Meter.query.filter_by(equipment_id=equipment.id, is_active=True).order_by(Meter.current_value.desc()).first()
            base_value = meter.last_maintenance_value if meter else None
            used_since_maintenance = (meter.current_value - (base_value or meter.initial_value or 0)) if meter else 0
            if not meter or task.meter_threshold is None or used_since_maintenance < task.meter_threshold:
                continue
        if equipment.id in existing_by_equipment:
            continue
        last = last_completions.get(equipment.id)
        interval = task.jours_entre_interventions or 0
        due = last + timedelta(days=interval) if last and interval else today
        if task.trigger_type == 'calendar' and due > today + timedelta(days=max(task.advance_days or 0, 0)):
            continue
        if task.trigger_type == 'season':
            completed_this_season = ScheduledTask.query.filter(
                ScheduledTask.lot_task_id == task.id,
                ScheduledTask.equipment_id == equipment.id,
                ScheduledTask.status.in_(['completed', 'done']),
                db.extract('year', ScheduledTask.scheduled_date) == today.year,
            ).first()
            if completed_this_season:
                continue
        due = next_working_day(max(due, today))
        room = equipment.effective_room
        status = "suspended" if unavailable and not task.is_safety_critical and task.unavailable_policy == "suspend" else "planned"
        # Une règle sans durée reste visible comme « à renseigner », mais ne
        # doit pas être engagée automatiquement dans une journée.
        estimated_duration = task.duration_for_equipment(equipment)
        final_status = status if status == "suspended" else ("needs_duration" if not estimated_duration else status)
        item = ScheduledTask(lot_task_id=task.id, equipment_id=equipment.id,
                             room_id=room.id if room else None,
                             company_id=task.company_id,
                             contract_id=task.contract_id,
                             scheduled_date=due, estimated_duration=estimated_duration, status=final_status,
                             suspension_reason="Équipement indisponible" if status == "suspended" else None,
                             room_name_snapshot=room.name if room else None,
                             lot_name_snapshot=task.lot.name,
                             equipment_name_snapshot=equipment.name,
                             lifecycle_status_snapshot=equipment.lifecycle_status)
        db.session.add(item)
        created.append(item)
    if commit:
        db.session.commit()
    elif created:
        db.session.flush()
    return created


def reconcile_planned_tasks():
    """Déplace les échéances ouvertes placées sur un jour non travaillé."""
    changed = []
    tasks = ScheduledTask.query.filter(ScheduledTask.status.in_(['planned', 'in_progress'])).all()
    for item in tasks:
        if item.scheduled_date and not PlanningService.get_working_hours(item.scheduled_date, user_id=item.assigned_to_id):
            item.scheduled_date = next_working_day(item.scheduled_date + timedelta(days=1))
            changed.append(item)
    if changed:
        db.session.commit()
    return changed
