from decimal import Decimal

from django.db import transaction
from django.db.models import Sum

from apps.materials.stock_service import create_reserves_for_plan, delete_reserves_for_plan
from apps.materials.models import StockTransaction
from apps.workrecords.services import sync_spreading_work_record
from .models import FertilizationEntry, FertilizationPlan, SpreadingSessionItem


class FertilizationPlanMergeError(Exception):
    pass


class FertilizationPlanMergeConflict(FertilizationPlanMergeError):
    def __init__(self, conflicts):
        super().__init__('merge conflict')
        self.conflicts = conflicts


def sync_actual_bags_for_pairs(year, field_fertilizer_pairs):
    pairs = {
        (int(field_id), int(fertilizer_id))
        for field_id, fertilizer_id in field_fertilizer_pairs
    }
    if not pairs:
        return

    for field_id, fertilizer_id in pairs:
        total = (
            SpreadingSessionItem.objects.filter(
                session__year=year,
                field_id=field_id,
                fertilizer_id=fertilizer_id,
            ).aggregate(total=Sum('actual_bags'))['total']
        )
        FertilizationEntry.objects.filter(
            plan__year=year,
            field_id=field_id,
            fertilizer_id=fertilizer_id,
        ).update(actual_bags=total)


@transaction.atomic
def sync_spreading_session_side_effects(session, field_fertilizer_pairs):
    sync_actual_bags_for_pairs(session.year, field_fertilizer_pairs)
    sync_stock_uses_for_spreading_session(session)
    sync_spreading_work_record(session)


@transaction.atomic
def sync_stock_uses_for_spreading_session(session):
    StockTransaction.objects.filter(spreading_item__session=session).delete()

    session_items = session.items.select_related('fertilizer__material')
    for item in session_items:
        material = getattr(item.fertilizer, 'material', None)
        if material is None:
            continue
        StockTransaction.objects.create(
            material=material,
            transaction_type=StockTransaction.TransactionType.USE,
            quantity=item.actual_bags,
            occurred_on=session.date,
            note=f'散布実績「{session.name.strip() or session.date}」',
            fertilization_plan=None,
            spreading_item=item,
        )


@transaction.atomic
def move_fertilization_entries_for_variety_change(change):
    moved_count = 0
    old_variety_id = change.old_variety_id
    new_variety = change.new_variety
    if old_variety_id is None or new_variety is None:
        return 0

    old_plans = (
        FertilizationPlan.objects
        .filter(
            year=change.year,
            variety_id=old_variety_id,
            entries__field_id=change.field_id,
        )
        .distinct()
        .prefetch_related('entries')
    )

    for old_plan in old_plans:
        entries_to_move = list(
            old_plan.entries.filter(
                field_id=change.field_id,
            ).order_by('id')
        )
        if not entries_to_move:
            continue

        new_plan = FertilizationPlan.objects.create(
            name=f'{change.year}年度 {new_variety.name} 施肥計画（品種変更移動）',
            year=change.year,
            variety=new_variety,
            calc_settings=old_plan.calc_settings,
        )

        FertilizationEntry.objects.filter(
            id__in=[entry.id for entry in entries_to_move]
        ).update(plan=new_plan)

        create_reserves_for_plan(old_plan)
        create_reserves_for_plan(new_plan)
        moved_count += len(entries_to_move)

    return moved_count


@transaction.atomic
def merge_fertilization_plan_into(source_plan, target_plan):
    if source_plan.id == target_plan.id:
        raise FertilizationPlanMergeError('同じ施肥計画にはマージできません。')
    if source_plan.year != target_plan.year:
        raise FertilizationPlanMergeError('年度が異なる施肥計画にはマージできません。')
    if source_plan.variety_id != target_plan.variety_id:
        raise FertilizationPlanMergeError('品種が異なる施肥計画にはマージできません。')
    if source_plan.is_confirmed or target_plan.is_confirmed:
        raise FertilizationPlanMergeError('散布確定済みの施肥計画はマージできません。')

    source_entries = list(
        source_plan.entries.select_related('field', 'fertilizer').order_by('field_id', 'fertilizer_id')
    )
    if not source_entries:
        raise FertilizationPlanMergeError('移動元の施肥計画にマージ対象の entry がありません。')

    source_pairs = {(entry.field_id, entry.fertilizer_id) for entry in source_entries}
    target_entries = list(
        target_plan.entries.select_related('field', 'fertilizer').order_by('field_id', 'fertilizer_id')
    )
    target_pairs = {(entry.field_id, entry.fertilizer_id): entry for entry in target_entries}

    conflicts = [
        {
            'field_id': entry.field_id,
            'field_name': entry.field.name,
            'fertilizer_id': entry.fertilizer_id,
            'fertilizer_name': entry.fertilizer.name,
        }
        for entry in source_entries
        if (entry.field_id, entry.fertilizer_id) in target_pairs
    ]
    if conflicts:
        raise FertilizationPlanMergeConflict(conflicts)

    FertilizationEntry.objects.filter(
        id__in=[entry.id for entry in source_entries]
    ).update(plan=target_plan)

    target_plan.calc_settings = _merge_calc_settings(
        target_plan.calc_settings,
        source_plan.calc_settings,
    )
    target_plan.save()

    create_reserves_for_plan(target_plan)

    moved_count = len(source_entries)
    deleted_source_plan = False
    if not FertilizationEntry.objects.filter(plan=source_plan).exists():
        delete_reserves_for_plan(source_plan)
        source_plan.delete()
        deleted_source_plan = True
    else:
        create_reserves_for_plan(source_plan)

    return {
        'moved_entry_count': moved_count,
        'deleted_source_plan': deleted_source_plan,
    }


def _merge_calc_settings(target_settings, source_settings):
    merged = list(target_settings or [])
    existing_fertilizer_ids = {
        setting.get('fertilizer_id')
        for setting in merged
        if isinstance(setting, dict)
    }
    for setting in source_settings or []:
        if not isinstance(setting, dict):
            continue
        fertilizer_id = setting.get('fertilizer_id')
        if fertilizer_id in existing_fertilizer_ids:
            continue
        merged.append(setting)
        existing_fertilizer_ids.add(fertilizer_id)
    return merged
