from collections import defaultdict
from decimal import Decimal
import bisect
import logging
import os

from django.db import transaction

from apps.business.distributor.models import DistributorID
from apps.business.rewards.models import DistributorLevelProgress
from apps.business.schemes.models import OpportunityBundleDistribution, OpportunityBundleLevel
from apps.business.wallet.models import UserVoucherCode
from apps.business.wallet.services.reward_processor import process_level_reward


# ── Binary subtree count (HYBRID GLOBAL ASSISTED BINARY — primary metric) ────

def _build_binary_subtree_count_map(distributors):
    """
    For each distributor, count ALL nodes in their isolated binary subtree
    (via binary_parent_distributor links).

    This is the authoritative metric for level completion in the
    Hybrid Global Assisted Binary system.

    The old global-position count is retained separately for analytics/admin.
    """
    result = {}
    root_ids = [d.id for d in distributors]

    def _chunked(values, size=500):
        for index in range(0, len(values), size):
            yield values[index:index + size]

    children_by_parent = defaultdict(list)
    seen = set(root_ids)
    frontier = list(root_ids)

    while frontier:
        rows = []
        for chunk in _chunked(frontier):
            rows.extend(
                DistributorID.objects.filter(
                    binary_parent_distributor_id__in=chunk
                ).values_list('id', 'binary_parent_distributor_id')
            )
        next_frontier = []
        for child_id, parent_id in rows:
            if child_id in seen:
                continue
            seen.add(child_id)
            children_by_parent[parent_id].append(child_id)
            next_frontier.append(child_id)
        frontier = next_frontier

    memo = {}

    def _count(nid):
        if nid in memo:
            return memo[nid]
        total = sum(1 + _count(cid) for cid in children_by_parent.get(nid, []))
        memo[nid] = total
        return total

    for d in distributors:
        result[d.id] = _count(d.id)

    return result


def _build_cumulative_levels(levels):
    cumulative = 0
    entries = []
    for level in levels:
        cumulative += int(getattr(level, 'required_positions', 0) or 0)
        entries.append(
            {
                'level_number': level.level_number,
                'required_positions': int(getattr(level, 'required_positions', 0) or 0),
                'cumulative_required_positions': cumulative,
            }
        )
    return entries


def _build_global_position_count_map(distributors):
    """
    Count all DistributorIDs in the same opportunity bundle whose global_position
    is greater than each distributor's own global_position.

    This reflects the business rule: level progression is driven by the number of
    new bundle purchases made system-wide *after* a distributor ID was assigned its
    sequential global position — by any user, not just the owner's referral tree.
    """
    result = {}
    bundle_to_dists = defaultdict(list)

    for dist in distributors:
        bundle_id = getattr(getattr(dist, 'product', None), 'opportunity_bundle_id', None)
        if bundle_id and dist.global_position:
            bundle_to_dists[bundle_id].append(dist)
        else:
            result[dist.id] = 0

    for bundle_id, dists in bundle_to_dists.items():
        # Fetch every global_position in this bundle and sort ascending once.
        all_positions = sorted(
            DistributorID.objects.filter(
                product__opportunity_bundle_id=bundle_id
            ).values_list('global_position', flat=True)
        )
        total = len(all_positions)
        for dist in dists:
            # bisect_right returns the insertion point *after* dist.global_position,
            # so everything to the right has global_position > dist.global_position.
            idx = bisect.bisect_right(all_positions, dist.global_position)
            result[dist.id] = total - idx

    return result


def _resolve_progress_snapshot(total_positions, cumulative_levels):
    highest_level = 0
    current_threshold = 0
    next_level = None
    next_required = None

    for level in cumulative_levels:
        threshold = level['cumulative_required_positions']
        if total_positions >= threshold:
            highest_level = level['level_number']
            current_threshold = threshold
            continue

        next_level = level['level_number']
        next_required = threshold
        break

    if next_required is not None:
        span = max(1, next_required - current_threshold)
        filled = max(0, total_positions - current_threshold)
        progress_percent = round(min(100.0, filled / span * 100), 1)
        positions_to_next = max(0, next_required - total_positions)
    elif cumulative_levels:
        progress_percent = 100.0
        positions_to_next = 0
    else:
        progress_percent = 0.0
        positions_to_next = 0

    return {
        'current_level': highest_level,
        'current_level_required_positions': current_threshold,
        'next_level': next_level,
        'next_level_required': next_required,
        'positions_to_next': positions_to_next,
        'progress_percent': progress_percent,
    }


def is_level_reward_coin_utilization_complete(distributor):
    """
    For distributor IDs generated from TWM_COINS products, require complete
    utilization of purchased voucher coins before level rewards can be unlocked.

    Returns True for non-TWM products, or when no unutilized purchased voucher
    code remains for this distributor's purchase order.
    """
    from apps.business.orders.models import Order

    if not distributor or not getattr(distributor, 'product', None):
        return True

    product = distributor.product
    if getattr(product, 'product_type', None) != 'TWM_COINS':
        return True

    order = Order.objects.filter(
        distributor=distributor,
        user=distributor.user,
        status='COMPLETED',
    ).only('reference_id').first()
    if not order:
        return True

    source_reference = f"{order.reference_id}-VCH"
    has_unutilized = UserVoucherCode.objects.filter(
        user=distributor.user,
        source_reference_id=source_reference,
        remaining_value__gt=Decimal('0.00'),
    ).exclude(status=UserVoucherCode.Status.EXPIRED).exists()
    return not has_unutilized


def calculate_level_progress_snapshots(distributors, persist=False):
    distributors = list(distributors)
    if not distributors:
        return {}

    # HYBRID GLOBAL ASSISTED BINARY:
    # Level completion is now driven by the size of each distributor's own
    # isolated binary subtree, not by global positions after theirs.
    # Global position is retained for visibility/ordering/analytics only.
    position_count_map = _build_binary_subtree_count_map(distributors)

    bundle_ids = {
        d.product.opportunity_bundle_id
        for d in distributors
        if getattr(d, 'product', None) and getattr(d.product, 'opportunity_bundle_id', None)
    }
    levels_by_bundle = defaultdict(list)
    if bundle_ids:
        for level in OpportunityBundleLevel.objects.filter(
            opportunity_bundle_id__in=bundle_ids
        ).order_by('opportunity_bundle_id', 'level_number'):
            levels_by_bundle[level.opportunity_bundle_id].append(level)

    existing_progress_map = {
        progress.distributor_id: progress
        for progress in DistributorLevelProgress.objects.filter(distributor__in=distributors)
    }

    snapshots = {}
    for distributor in distributors:
        total_positions = position_count_map.get(distributor.id, 0)
        opportunity_bundle_id = getattr(getattr(distributor, 'product', None), 'opportunity_bundle_id', None)
        cumulative_levels = _build_cumulative_levels(levels_by_bundle.get(opportunity_bundle_id, []))
        snapshot = _resolve_progress_snapshot(total_positions, cumulative_levels)

        existing_progress = existing_progress_map.get(distributor.id)
        last_completed_level = 0
        if existing_progress:
            last_completed_level = min(existing_progress.last_completed_level, snapshot['current_level'])

        snapshots[distributor.id] = {
            **snapshot,
            'total_positions_after': total_positions,
            'last_completed_level': last_completed_level,
            'last_calculated_at': getattr(existing_progress, 'last_calculated_at', None),
        }

        if persist:
            DistributorLevelProgress.objects.update_or_create(
                distributor=distributor,
                defaults={
                    'current_level': snapshot['current_level'],
                    'last_completed_level': last_completed_level,
                    'total_positions_after': total_positions,
                },
            )

    return snapshots


def process_auto_id_generation(distributor_id, level):
    """Create auto-ID allocation entries after level completion.

    If AUTO_ID_AUTO_ACTIVATION_ENABLED=true, this falls back to immediate activation
    for backward compatibility.
    """
    from apps.business.distributor.models import AutoIDAllocation
    from apps.business.distributor.services.hybrid_service import activate_auto_ids

    auto_ids_created = 0
    total_cost = Decimal('0.00')
    auto_id_count = getattr(level, 'auto_id_count', 0)
    auto_id_product = getattr(level, 'auto_id_product', None)
    if not getattr(level, 'auto_id_enabled', False) or auto_id_count <= 0 or not auto_id_product:
        return {"auto_ids_created": 0, "total_cost": Decimal('0.00')}

    if getattr(distributor_id, 'is_auto_generated', False):
        return {"auto_ids_created": 0, "total_cost": Decimal('0.00')}

    opportunity_bundle = getattr(auto_id_product, 'opportunity_bundle', None)
    base_amount = None
    if opportunity_bundle:
        dist_entry = OpportunityBundleDistribution.objects.filter(
            opportunity_bundle=opportunity_bundle,
            component_type='DIRECT_INCENTIVE',
        ).first()
        if dist_entry:
            base_amount = dist_entry.value

    if base_amount is None:
        base_amount = getattr(auto_id_product, 'base_cost', None)
    if base_amount is None or Decimal(base_amount) <= 0:
        return {"auto_ids_created": 0, "total_cost": Decimal('0.00')}

    total_cost = Decimal(auto_id_count) * Decimal(base_amount)
    with transaction.atomic():
        allocation = AutoIDAllocation.objects.create(
            owner_user=distributor_id.user,
            source_distributor=distributor_id,
            level_number=level.level_number,
            total_count=auto_id_count,
            consumed_count=0,
            remaining_count=auto_id_count,
            auto_id_balance=total_cost,
            per_auto_id_value=Decimal(base_amount),
            eligible_product=auto_id_product,
            status=AutoIDAllocation.Status.PENDING,
        )

        # Optional compatibility mode: immediately activate allocated IDs.
        auto_activate = os.getenv('AUTO_ID_AUTO_ACTIVATION_ENABLED', 'false').lower() in ('1', 'true', 'yes', 'on')
        if auto_activate:
            try:
                result = activate_auto_ids(
                    user=distributor_id.user,
                    allocation=allocation,
                    count=auto_id_count,
                    product=auto_id_product,
                )
                auto_ids_created = int(result.get('created_count') or 0)
            except Exception as exc:
                logging.error(
                    "Failed to auto-activate IDs: distributor_id=%s level=%s product=%s error=%s",
                    distributor_id.id,
                    level.level_number,
                    getattr(auto_id_product, 'id', None),
                    exc,
                )

    return {"auto_ids_created": auto_ids_created, "total_cost": total_cost}


def process_level_rewards(distributor_id):
    """Process pending level rewards for the distributor's newly completed levels."""
    calculate_level_progress(distributor_id)
    progress = DistributorLevelProgress.objects.filter(distributor=distributor_id).first()
    if not progress:
        return {"levels_rewarded": [], "total_reward": Decimal('0.00'), "deductions": []}

    current_level = progress.current_level
    last_completed_level = progress.last_completed_level
    if current_level <= last_completed_level:
        return {"levels_rewarded": [], "total_reward": Decimal('0.00'), "deductions": []}

    opportunity_bundle = getattr(distributor_id.product, 'opportunity_bundle', None)
    if not opportunity_bundle:
        return {"levels_rewarded": [], "total_reward": Decimal('0.00'), "deductions": []}

    coin_utilization_complete = is_level_reward_coin_utilization_complete(distributor_id)
    if not coin_utilization_complete:
        logging.info(
            "Level reward unlock blocked: distributor_id=%s product=%s reason=coin_utilization_pending",
            distributor_id.id,
            getattr(getattr(distributor_id, 'product', None), 'name', None),
        )
        return {
            "levels_rewarded": [],
            "total_reward": Decimal('0.00'),
            "deductions": [],
            "blocked_reason": "COIN_UTILIZATION_PENDING",
            "coin_utilization_complete": False,
        }

    levels = OpportunityBundleLevel.objects.filter(
        opportunity_bundle=opportunity_bundle,
        level_number__gt=last_completed_level,
        level_number__lte=current_level,
    ).order_by('level_number')

    levels_rewarded = []
    total_net_amount = Decimal('0.00')
    deduction_details = []

    with transaction.atomic():
        for level in levels:
            reward_amount = level.reward_amount
            if reward_amount is None or reward_amount <= 0:
                continue
            try:
                reward_result = process_level_reward(
                    user=distributor_id.user,
                    distributor_id=distributor_id.id,
                    level_number=level.level_number,
                    reward_amount=reward_amount,
                )
            except Exception as exc:
                logging.error(
                    "Failed to credit reward: distributor_id=%s level=%s reward_amount=%s error=%s",
                    distributor_id.id,
                    level.level_number,
                    reward_amount,
                    exc,
                )
                continue

            net_amount = Decimal(str(reward_result.get("super_coin_credit", "0")))
            levels_rewarded.append(level.level_number)
            total_net_amount += net_amount
            deduction_details.append(reward_result)

        progress.last_completed_level = current_level
        progress.save(update_fields=['last_completed_level'])

    return {
        "levels_rewarded": levels_rewarded,
        "total_reward": total_net_amount,
        "deductions": deduction_details,
        "coin_utilization_complete": True,
    }


def calculate_level_progress(distributor_id):
    """Calculate and persist distributor level progress from descendant positions under this distributor ID."""
    snapshot = calculate_level_progress_snapshots([distributor_id], persist=True).get(distributor_id.id, {})
    return {
        'current_level': snapshot.get('current_level', 0),
        'positions_after': snapshot.get('total_positions_after', 0),
        'last_completed_level': snapshot.get('last_completed_level', 0),
    }
