from time import sleep
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

from flask import Blueprint, request, jsonify
from tinydb import Query

from api.child_rewards import ChildReward
from api.child_tasks import ChildTask
from api.pending_confirmation import PendingConfirmationResponse
from api.reward_status import RewardStatus
from api.utils import send_event_for_current_user, get_validated_user_id
import api.child_action_helpers as chore_actions
from db.db import child_db, task_db, reward_db, routine_db, pending_reward_db, pending_confirmations_db, users_db
from db.tracking import insert_tracking_event
from db.child_overrides import get_override, delete_override, delete_overrides_for_child
from events.types.child_chore_confirmation import ChildChoreConfirmation
from events.types.child_modified import ChildModified
from events.types.child_reward_request import ChildRewardRequest
from events.types.child_reward_triggered import ChildRewardTriggered
from events.types.child_rewards_set import ChildRewardsSet
from events.types.child_task_triggered import ChildTaskTriggered
from events.types.child_tasks_set import ChildTasksSet
from events.types.tracking_event_created import TrackingEventCreated
from events.types.event import Event
from events.types.event_types import EventType
from models.child import Child
from models.pending_confirmation import PendingConfirmation
from models.pending_reward import PendingReward
from models.reward import Reward
from models.task import Task
from models.tracking_event import TrackingEvent
from utils.tracking_logger import log_tracking_event
from utils.push_sender import send_push_to_user
from utils.digest_token import create_action_token
from collections import defaultdict
from db.chore_schedules import get_schedule
from db.task_extensions import get_extension_for_child_task
from db.routine_schedules import delete_schedules_for_child as delete_routine_schedules_for_child
from db.routine_extensions import delete_extensions_for_child as delete_routine_extensions_for_child
import logging

child_api = Blueprint('child_api', __name__)
logger = logging.getLogger(__name__)


def _get_user_timezone(user_id: str) -> str | None:
    user = users_db.get(Query().id == user_id)
    if not user:
        return None
    return user.get('timezone')


def _get_user_today_local(user_id: str) -> tuple[str, str | None]:
    tz_str = _get_user_timezone(user_id)
    try:
        now_local = datetime.now(ZoneInfo(tz_str)) if tz_str else datetime.now(timezone.utc)
    except Exception:
        tz_str = None
        now_local = datetime.now(timezone.utc)
    return now_local.strftime('%Y-%m-%d'), tz_str


def _is_iso_timestamp_on_local_day(timestamp: str | None, local_day: str, tz_str: str | None) -> bool:
    if not timestamp:
        return False
    try:
        normalized = timestamp.replace('Z', '+00:00')
        parsed = datetime.fromisoformat(normalized)
        if parsed.tzinfo is None:
            parsed = parsed.replace(tzinfo=timezone.utc)
        try:
            tz = ZoneInfo(tz_str) if tz_str else timezone.utc
        except Exception:
            tz = timezone.utc
        return parsed.astimezone(tz).strftime('%Y-%m-%d') == local_day
    except (TypeError, ValueError):
        return False


def _is_epoch_timestamp_on_local_day(epoch_ts, local_day: str, tz_str: str | None) -> bool:
    if epoch_ts is None:
        return False
    try:
        tz = ZoneInfo(tz_str) if tz_str else timezone.utc
    except Exception:
        tz = timezone.utc
    try:
        return datetime.fromtimestamp(float(epoch_ts), tz).strftime('%Y-%m-%d') == local_day
    except (TypeError, ValueError, OSError):
        return False

@child_api.route('/child/<name>', methods=['GET'])
@child_api.route('/child/<id>', methods=['GET'])
def get_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    return jsonify(Child.from_dict(result[0]).to_dict()), 200

@child_api.route('/child/add', methods=['PUT'])
def add_child():
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    name = data.get('name')
    age = data.get('age')
    image = data.get('image_id', None)
    if not name:
        return jsonify({'error': 'Name is required'}), 400
    if not image:
        image = 'boy01'

    child = Child(name=name, age=age, image_id=image, user_id=user_id)
    child_db.insert(child.to_dict())
    resp = send_event_for_current_user(
        Event(EventType.CHILD_MODIFIED.value, ChildModified(child.id, ChildModified.OPERATION_ADD)))
    if resp:
        return resp
    return jsonify({'message': f'Child {name} added.'}), 201

@child_api.route('/child/<id>/edit', methods=['PUT'])
def edit_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    name = data.get('name', None)
    age = data.get('age', None)
    points = data.get('points', None)
    image = data.get('image_id', None)
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    child = Child.from_dict(result[0])

    if name is not None:
        child.name = name
    if age is not None:
        child.age = age
    if points is not None:
        child.points = points
    if image is not None:
        child.image_id = image

    # Check if points changed and handle pending rewards
    if points is not None:
        PendingQuery = Query()
        pending_rewards = pending_confirmations_db.search(
            (PendingQuery.child_id == id) & (PendingQuery.user_id == user_id) &
            (PendingQuery.entity_type == 'reward') & (PendingQuery.status == 'pending')
        )

        RewardQuery = Query()
        for pr in pending_rewards:
            pending = PendingConfirmation.from_dict(pr)
            reward_result = reward_db.get((RewardQuery.id == pending.entity_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
            if reward_result:
                reward = Reward.from_dict(reward_result)
                # If child can no longer afford the reward, remove the pending request
                if child.points < reward.cost:
                    pending_confirmations_db.remove(
                        (PendingQuery.child_id == id) & (PendingQuery.entity_id == reward.id) &
                        (PendingQuery.entity_type == 'reward') & (PendingQuery.user_id == user_id)
                    )
                    resp = send_event_for_current_user(
                        Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(id, reward.id, ChildRewardRequest.REQUEST_CANCELLED)))
                    if resp:
                        return resp
    child_db.update(child.to_dict(), ChildQuery.id == id)
    resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_EDIT)))
    if resp:
        return resp
    return jsonify({'message': f'Child {id} updated.'}), 200

@child_api.route('/child/list', methods=['GET'])
def list_children():
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    children = child_db.search(ChildQuery.user_id == user_id)
    return jsonify({'children': children}), 200

# Child DELETE
@child_api.route('/child/<id>', methods=['DELETE'])
def delete_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    
    # Cascade delete overrides for this child
    deleted_count = delete_overrides_for_child(id)
    if deleted_count > 0:
        logger.info(f"Cascade deleted {deleted_count} overrides for child {id}")

    # Cascade delete routine schedule/extension rows for this child.
    delete_routine_schedules_for_child(id)
    delete_routine_extensions_for_child(id)

    # Remove pending routine confirmations for this child.
    PendingQuery = Query()
    pending_confirmations_db.remove((PendingQuery.child_id == id) & (PendingQuery.entity_type == 'routine'))
    
    if child_db.remove((ChildQuery.id == id) & (ChildQuery.user_id == user_id)):
        resp = send_event_for_current_user(Event(EventType.CHILD_MODIFIED.value, ChildModified(id, ChildModified.OPERATION_DELETE)))
        if resp:
            return resp
        return jsonify({'message': f'Child {id} deleted.'}), 200
    return jsonify({'error': 'Child not found'}), 404

@child_api.route('/child/<id>/assign-task', methods=['POST'])
def assign_task_to_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    if task_id not in child.get('tasks', []):
        child['tasks'].append(task_id)
        child_db.update({'tasks': child['tasks']}, ChildQuery.id == id)
    return jsonify({'message': f"Task {task_id} assigned to {child.get('name')}."}), 200

# python
@child_api.route('/child/<id>/set-tasks', methods=['PUT'])
def set_child_tasks(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json() or {}
    task_ids = data.get('task_ids')
    if 'type' not in data:
        return jsonify({'error': 'type is required (chore, kindness, or penalty)'}), 400
    task_type = data.get('type')
    if task_type not in ['chore', 'kindness', 'penalty']:
        return jsonify({'error': 'type must be chore, kindness, or penalty', 'code': 'INVALID_TASK_TYPE'}), 400
    if not isinstance(task_ids, list):
        return jsonify({'error': 'task_ids must be a list'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    child = Child.from_dict(result[0])
    new_task_ids = set(task_ids)

    # Add all existing child tasks of other types
    for task_record in task_db.all():
        task_obj = Task.from_dict(task_record)
        if task_obj.id in child.tasks and task_obj.type != task_type:
            new_task_ids.add(task_obj.id)

    # Convert back to list if needed
    new_tasks = list(new_task_ids)
    
    # Identify unassigned tasks and delete their overrides and pending confirmations
    old_task_ids = set(child.tasks)
    unassigned_task_ids = old_task_ids - new_task_ids
    for task_id in unassigned_task_ids:
        # Only delete overrides for task entities
        override = get_override(id, task_id)
        if override and override.entity_type == 'task':
            delete_override(id, task_id)
            logger.info(f"Deleted override for unassigned task: child={id}, task={task_id}")
        # Clear any pending chore confirmation
        PendingQ = Query()
        pending_confirmations_db.remove(
            (PendingQ.child_id == id) & (PendingQ.entity_id == task_id) &
            (PendingQ.entity_type == 'chore') & (PendingQ.user_id == user_id)
        )
    
    # Replace tasks with validated IDs
    child_db.update({'tasks': new_tasks}, ChildQuery.id == id)
    resp = send_event_for_current_user(Event(EventType.CHILD_TASKS_SET.value, ChildTasksSet(id, new_tasks)))
    if resp:
        return resp
    return jsonify({
        'message': f'Tasks set for child {id}.',
        'task_ids': new_tasks,
        'count': len(new_tasks)
    }), 200



@child_api.route('/child/<id>/remove-task', methods=['POST'])
def remove_task_from_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    if task_id in child.get('tasks', []):
        child['tasks'].remove(task_id)
        child_db.update({'tasks': child['tasks']}, ChildQuery.id == id)
        # Clear any pending chore confirmation for this task
        PendingQ = Query()
        pending_confirmations_db.remove(
            (PendingQ.child_id == id) & (PendingQ.entity_id == task_id) &
            (PendingQ.entity_type == 'chore') & (PendingQ.user_id == user_id)
        )
        return jsonify({'message': f'Task {task_id} removed from {child["name"]}.'}), 200
    return jsonify({'error': 'Task not assigned to child'}), 400

@child_api.route('/child/<id>/list-tasks', methods=['GET'])
def list_child_tasks(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    task_ids = child.get('tasks', [])

    TaskQuery = Query()
    today_local, tz_str = _get_user_today_local(user_id)
    child_tasks = []
    for tid in task_ids:
        task = task_db.get((TaskQuery.id == tid) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
        if not task:
            continue
        
        task_obj = Task.from_dict(task)
        
        # Check for override
        override = get_override(id, tid)
        custom_value = override.custom_value if override else None
        
        ct = ChildTask(task_obj.name, task_obj.type, task_obj.points, task_obj.image_id, task_obj.id)
        ct_dict = ct.to_dict()
        if custom_value is not None:
            ct_dict['custom_value'] = custom_value

        # Attach schedule and most recent extension_date for chores (client does all time math)
        if task_obj.type == 'chore':
            schedule = get_schedule(id, tid)
            ct_dict['schedule'] = schedule.to_dict() if schedule else None
            ext = get_extension_for_child_task(id, tid)
            ct_dict['extension_date'] = ext.date if ext else None
            
            # Attach pending confirmation status for chores
            PendingQuery = Query()
            pending = pending_confirmations_db.get(
                (PendingQuery.child_id == id) & (PendingQuery.entity_id == tid) &
                (PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
            )
            if pending:
                status = pending.get('status')
                approved_at = pending.get('approved_at')
                created_at = pending.get('created_at')
                if status == 'approved' and _is_iso_timestamp_on_local_day(approved_at, today_local, tz_str):
                    ct_dict['pending_status'] = 'approved'
                    ct_dict['approved_at'] = approved_at
                elif status == 'pending' and _is_epoch_timestamp_on_local_day(created_at, today_local, tz_str):
                    ct_dict['pending_status'] = 'pending'
                    ct_dict['approved_at'] = None
                else:
                    pending_id = pending.get('id')
                    if pending_id:
                        pending_confirmations_db.remove(PendingQuery.id == pending_id)
                    ct_dict['pending_status'] = None
                    ct_dict['approved_at'] = None
            else:
                ct_dict['pending_status'] = None
                ct_dict['approved_at'] = None
        else:
            ct_dict['schedule'] = None
            ct_dict['extension_date'] = None
            ct_dict['pending_status'] = None
            ct_dict['approved_at'] = None

        child_tasks.append(ct_dict)

    return jsonify({'tasks': child_tasks}), 200

@child_api.route('/child/<id>/list-assignable-tasks', methods=['GET'])
def list_assignable_tasks(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    assigned_ids = set(child.get('tasks', []))

    # Get all assignable tasks (not already assigned)
    all_tasks = [t for t in task_db.all() if t and t.get('id') and t.get('id') not in assigned_ids]

    # Group by name
    name_to_tasks = defaultdict(list)
    for t in all_tasks:
        name_to_tasks[t.get('name')].append(t)

    filtered_tasks = []
    for name, tasks in name_to_tasks.items():
        user_tasks = [t for t in tasks if t.get('user_id') is not None]
        if len(user_tasks) == 0:
            # Only system task exists
            filtered_tasks.append(tasks[0])
        elif len(user_tasks) == 1:
            # Only one user task: show it, not system
            filtered_tasks.append(user_tasks[0])
        else:
            # Multiple user tasks: show all user tasks, not system
            filtered_tasks.extend(user_tasks)

    # Wrap in ChildTask and return
    assignable_tasks = [ChildTask(t.get('name'), Task.from_dict(t).type, t.get('points'), t.get('image_id'), t.get('id')).to_dict() for t in filtered_tasks]
    return jsonify({'tasks': assignable_tasks, 'count': len(assignable_tasks)}), 200


@child_api.route('/child/<id>/list-all-tasks', methods=['GET'])
def list_all_tasks(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    has_type = "type" in request.args
    if has_type and request.args.get('type') not in ['chore', 'kindness', 'penalty']:
        return jsonify({'error': 'type must be chore, kindness, or penalty'}), 400
    filter_type = request.args.get('type', None) if has_type else None

    child = result[0]
    assigned_ids = set(child.get('tasks', []))
    # Get all tasks from database (not filtering out assigned, since this is 'all')
    ChildTaskQuery = Query()
    all_tasks = task_db.search((ChildTaskQuery.user_id == user_id) | (ChildTaskQuery.user_id == None))

    name_to_tasks = defaultdict(list)
    for t in all_tasks:
        name_to_tasks[t.get('name')].append(t)

    filtered_tasks = []
    for name, tasks in name_to_tasks.items():
        user_tasks = [t for t in tasks if t.get('user_id') is not None]
        if len(user_tasks) == 0:
            filtered_tasks.append(tasks[0])
        elif len(user_tasks) == 1:
            filtered_tasks.append(user_tasks[0])
        else:
            filtered_tasks.extend(user_tasks)
            
    result_tasks = []
    for t in filtered_tasks:
        task_obj = Task.from_dict(t)
        if has_type and task_obj.type != filter_type:
            continue
        ct = ChildTask(
            task_obj.name,
            task_obj.type,
            task_obj.points,
            task_obj.image_id,
            task_obj.id
        )
        task_dict = ct.to_dict()
        task_dict.update({'assigned': t.get('id') in assigned_ids})
        result_tasks.append(task_dict)            
        
    result_tasks.sort(key=lambda t: (not t['assigned'], t['name'].lower()))
    return jsonify({ 'tasks': result_tasks, 'count': len(result_tasks), 'list_type': 'task' }), 200


@child_api.route('/child/<id>/trigger-task', methods=['POST'])
def trigger_child_task(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child: Child = Child.from_dict(result[0])
    if task_id not in child.tasks:
        return jsonify({'error': f'Task not found assigned to child {child.name}'}), 404
    # look up  the task and get the details
    TaskQuery = Query()
    task_result = task_db.search((TaskQuery.id == task_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
    if not task_result:
        return jsonify({'error': 'Task not found in task database'}), 404
    task: Task = Task.from_dict(task_result[0])
    
    # Capture points before modification
    points_before = child.points
    
    # Check for override
    override = get_override(id, task_id)
    points_value = override.custom_value if override else task.points
    
    # update the child's points based on task type
    if task.is_good:
        child.points += points_value
    else:
        child.points -= points_value
        child.points = max(child.points, 0)
    
    # update the child in the database
    child_db.update({'points': child.points}, ChildQuery.id == id)
    
    # For chores, create an approved PendingConfirmation so child view shows COMPLETED
    # Only persist for scheduled chores; general chores reset to normal after trigger
    if task.type == 'chore':
        PendingQuery = Query()
        # Remove any existing pending confirmation for this chore
        pending_confirmations_db.remove(
            (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
            (PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
        )
        schedule = get_schedule(id, task_id)
        if schedule:
            confirmation = PendingConfirmation(
                child_id=id, entity_id=task_id, entity_type='chore',
                user_id=user_id, status='approved',
                approved_at=datetime.now(timezone.utc).isoformat()
            )
            pending_confirmations_db.insert(confirmation.to_dict())
        send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
                                          ChildChoreConfirmation(id, task_id, ChildChoreConfirmation.OPERATION_APPROVED)))
    
    # Create tracking event
    entity_type = task.type
    tracking_metadata = {
        'task_name': task.name, 
        'task_type': task.type,
        'default_points': task.points
    }
    if override:
        tracking_metadata['custom_points'] = override.custom_value
        tracking_metadata['has_override'] = True
    
    tracking_event = TrackingEvent.create_event(
        user_id=user_id,
        child_id=child.id,
        entity_type=entity_type,
        entity_id=task.id,
        action='activated',
        points_before=points_before,
        points_after=child.points,
        metadata=tracking_metadata
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)
    
    # Send tracking event via SSE
    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, entity_type, 'activated')))
    
    resp = send_event_for_current_user(Event(EventType.CHILD_TASK_TRIGGERED.value, ChildTaskTriggered(task.id, child.id,  child.points)))
    if resp:
        return resp
    return jsonify({'message': f'{task.name} points assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200

@child_api.route('/child/<id>/assign-reward', methods=['POST'])
def assign_reward_to_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    reward_id = data.get('reward_id')
    if not reward_id:
        return jsonify({'error': 'reward_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    if reward_id not in child.get('rewards', []):
        child['rewards'].append(reward_id)
        child_db.update({'rewards': child['rewards']}, ChildQuery.id == id)
    return jsonify({'message': f'Reward {reward_id} assigned to {child["name"]}.'}), 200

@child_api.route('/child/<id>/list-all-rewards', methods=['GET'])
def list_all_rewards(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = Child.from_dict(result[0])
    assigned_ids = set(child.rewards)

    # Get all rewards from database
    ChildRewardQuery = Query()
    all_rewards = reward_db.search((ChildRewardQuery.user_id == user_id) | (ChildRewardQuery.user_id == None))

    name_to_rewards = defaultdict(list)
    for r in all_rewards:
        name_to_rewards[r.get('name')].append(r)

    filtered_rewards = []
    for name, rewards in name_to_rewards.items():
        user_rewards = [r for r in rewards if r.get('user_id') is not None]
        if len(user_rewards) == 0:
            filtered_rewards.append(rewards[0])
        elif len(user_rewards) == 1:
            filtered_rewards.append(user_rewards[0])
        else:
            filtered_rewards.extend(user_rewards)

    result_rewards = []
    for r in filtered_rewards:
        cr = ChildReward(
            r.get('name'),
            r.get('cost'),
            r.get('image_id'),
            r.get('id')
        )
        reward_dict = cr.to_dict()
        reward_dict.update({'assigned': r.get('id') in assigned_ids})
        result_rewards.append(reward_dict)
    result_rewards.sort(key=lambda t: (not t['assigned'], t['name'].lower()))
    return jsonify({
        'rewards': result_rewards,
        'rewards_count': len(result_rewards),
        'list_type': 'reward'
    }), 200


@child_api.route('/child/<id>/set-rewards', methods=['PUT'])
def set_child_rewards(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json() or {}
    reward_ids = data.get('reward_ids')
    if not isinstance(reward_ids, list):
        return jsonify({'error': 'reward_ids must be a list'}), 400

    # Deduplicate and drop falsy values
    new_reward_ids = [rid for rid in dict.fromkeys(reward_ids) if rid]

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    
    child = Child.from_dict(result[0])
    old_reward_ids = set(child.rewards)

    # Optional: validate reward IDs exist in the reward DB
    RewardQuery = Query()
    valid_reward_ids = []
    for rid in new_reward_ids:
        if reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None))):
            valid_reward_ids.append(rid)
    
    # Identify unassigned rewards and delete their overrides and pending confirmations
    new_reward_ids_set = set(valid_reward_ids)
    unassigned_reward_ids = old_reward_ids - new_reward_ids_set
    for reward_id in unassigned_reward_ids:
        override = get_override(id, reward_id)
        if override and override.entity_type == 'reward':
            delete_override(id, reward_id)
            logger.info(f"Deleted override for unassigned reward: child={id}, reward={reward_id}")
        # Clear any pending reward confirmation
        PendingQ = Query()
        pending_confirmations_db.remove(
            (PendingQ.child_id == id) & (PendingQ.entity_id == reward_id) &
            (PendingQ.entity_type == 'reward') & (PendingQ.user_id == user_id)
        )

    # Replace rewards with validated IDs
    child_db.update({'rewards': valid_reward_ids}, ChildQuery.id == id)
    send_event_for_current_user(Event(EventType.CHILD_REWARDS_SET.value, ChildRewardsSet(id, valid_reward_ids)))
    return jsonify({
        'message': f'Rewards set for child {id}.',
        'reward_ids': valid_reward_ids,
        'count': len(valid_reward_ids)
    }), 200


@child_api.route('/child/<id>/remove-reward', methods=['POST'])
def remove_reward_from_child(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    reward_id = data.get('reward_id')
    if not reward_id:
        return jsonify({'error': 'reward_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    if reward_id in child.get('rewards', []):
        child['rewards'].remove(reward_id)
        child_db.update({'rewards': child['rewards']}, ChildQuery.id == id)
        # Clear any pending reward confirmation for this reward
        PendingQ = Query()
        pending_confirmations_db.remove(
            (PendingQ.child_id == id) & (PendingQ.entity_id == reward_id) &
            (PendingQ.entity_type == 'reward') & (PendingQ.user_id == user_id)
        )
        return jsonify({'message': f'Reward {reward_id} removed from {child["name"]}.'}), 200
    return jsonify({'error': 'Reward not assigned to child'}), 400

@child_api.route('/child/<id>/list-rewards', methods=['GET'])
def list_child_rewards(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    reward_ids = child.get('rewards', [])

    RewardQuery = Query()
    child_rewards = []
    for rid in reward_ids:
        reward = reward_db.get((RewardQuery.id == rid) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
        if not reward:
            continue
        
        # Check for override
        override = get_override(id, rid)
        custom_value = override.custom_value if override else None
        
        cr = ChildReward(reward.get('name'), reward.get('cost'), reward.get('image_id'), reward.get('id'))
        cr_dict = cr.to_dict()
        if custom_value is not None:
            cr_dict['custom_value'] = custom_value
        child_rewards.append(cr_dict)

    return jsonify({'rewards': child_rewards}), 200

@child_api.route('/child/<id>/list-assignable-rewards', methods=['GET'])
def list_assignable_rewards(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = result[0]
    assigned_ids = set(child.get('rewards', []))

    # Get all assignable rewards (not already assigned)
    all_rewards = [r for r in reward_db.all() if r and r.get('id') and r.get('id') not in assigned_ids]

    # Group by name
    name_to_rewards = defaultdict(list)
    for r in all_rewards:
        name_to_rewards[r.get('name')].append(r)

    filtered_rewards = []
    for name, rewards in name_to_rewards.items():
        user_rewards = [r for r in rewards if r.get('user_id') is not None]
        if len(user_rewards) == 0:
            filtered_rewards.append(rewards[0])
        elif len(user_rewards) == 1:
            filtered_rewards.append(user_rewards[0])
        else:
            filtered_rewards.extend(user_rewards)

    assignable_rewards = [ChildReward(r.get('name'), r.get('cost'), r.get('image_id'), r.get('id')).to_dict() for r in filtered_rewards]
    return jsonify({'rewards': assignable_rewards, 'count': len(assignable_rewards)}), 200

@child_api.route('/child/<id>/trigger-reward', methods=['POST'])
def trigger_child_reward(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    reward_id = data.get('reward_id')
    if not reward_id:
        return jsonify({'error': 'reward_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child: Child = Child.from_dict(result[0])
    if reward_id not in child.rewards:
        return jsonify({'error': f'Reward not found assigned to child {child.name}'}), 404
    # look up  the task and get the details
    RewardQuery = Query()
    reward_result = reward_db.search((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
    if not reward_result:
        return jsonify({'error': 'Reward not found in reward database'}), 404
    reward: Reward = Reward.from_dict(reward_result[0])

    # Check for override
    override = get_override(id, reward_id)
    cost_value = override.custom_value if override else reward.cost

    # Check if child has enough points
    logger.info(f'Child {child.name} has {child.points} points, reward {reward.name} costs {cost_value} points')
    if child.points < cost_value:
        points_needed = cost_value - child.points
        return jsonify({
            'error': 'Insufficient points',
            'points_needed': points_needed,
            'current_points': child.points,
            'reward_cost': cost_value
        }), 400

    # Remove matching pending reward requests for this child and reward
    PendingQuery = Query()
    removed = pending_confirmations_db.remove(
        (PendingQuery.child_id == child.id) & (PendingQuery.entity_id == reward.id) &
        (PendingQuery.entity_type == 'reward')
    )
    if removed:
        send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(reward.id, child.id, ChildRewardRequest.REQUEST_GRANTED)))

    # Capture points before modification
    points_before = child.points
    
    # update the child's points based on reward cost
    child.points -= cost_value
    # update the child in the database
    child_db.update({'points': child.points}, ChildQuery.id == id)
    
    # Create tracking event
    tracking_metadata = {
        'reward_name': reward.name, 
        'reward_cost': reward.cost,
        'default_cost': reward.cost
    }
    if override:
        tracking_metadata['custom_cost'] = override.custom_value
        tracking_metadata['has_override'] = True
    
    tracking_event = TrackingEvent.create_event(
        user_id=user_id,
        child_id=child.id,
        entity_type='reward',
        entity_id=reward.id,
        action='redeemed',
        points_before=points_before,
        points_after=child.points,
        metadata=tracking_metadata
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)
    
    # Send tracking event via SSE
    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, 'reward', 'redeemed')))
    
    send_event_for_current_user(Event(EventType.CHILD_REWARD_TRIGGERED.value, ChildRewardTriggered(reward.id, child.id, child.points)))
    return jsonify({'message': f'{reward.name} assigned to {child.name}.', 'points': child.points, 'id': child.id}), 200

@child_api.route('/child/<id>/affordable-rewards', methods=['GET'])
def list_affordable_rewards(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = Child.from_dict(result[0])
    points = child.points
    reward_ids = child.rewards
    RewardQuery = Query()
    affordable = [
        Reward.from_dict(reward).to_dict() for reward_id in reward_ids
        if (reward := reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))) and points >= Reward.from_dict(reward).cost
    ]
    return jsonify({'affordable_rewards': affordable}), 200

@child_api.route('/child/<id>/reward-status', methods=['GET'])
def reward_status(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = Child.from_dict(result[0])
    points = child.points
    reward_ids = child.rewards

    RewardQuery = Query()
    today_local, tz_str = _get_user_today_local(user_id)
    statuses = []
    for reward_id in reward_ids:
        reward_dict = reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
        if not reward_dict:
            continue
        reward: Reward = Reward.from_dict(reward_dict)
        
        # Check for override
        override = get_override(id, reward_id)
        cost_value = override.custom_value if override else reward.cost
        points_needed = max(0, cost_value - points)
        
        #check to see if this reward id and child id is in the pending confirmations db
        pending_query = Query()
        pending = pending_confirmations_db.get(
            (pending_query.child_id == child.id) & (pending_query.entity_id == reward.id) &
            (pending_query.entity_type == 'reward') & (pending_query.user_id == user_id)
        )
        redeeming = False
        if pending and pending.get('status') == 'pending':
            if _is_epoch_timestamp_on_local_day(pending.get('created_at'), today_local, tz_str):
                redeeming = True
            else:
                pending_id = pending.get('id')
                if pending_id:
                    pending_confirmations_db.remove(pending_query.id == pending_id)

        status = RewardStatus(reward.id, reward.name, points_needed, cost_value, redeeming, reward.image_id)
        status_dict = status.to_dict()
        if override:
            status_dict['custom_value'] = override.custom_value
        statuses.append(status_dict)

    statuses.sort(key=lambda s: (not s['redeeming'], s['cost']))
    return jsonify({'reward_status': statuses}), 200


@child_api.route('/child/<id>/request-reward', methods=['POST'])
def request_reward(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    reward_id = data.get('reward_id')
    if not reward_id:
        return jsonify({'error': 'reward_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = Child.from_dict(result[0])
    if reward_id not in child.rewards:
        return jsonify({'error': f'Reward not assigned to child {child.name}'}), 404

    RewardQuery = Query()
    reward_result = reward_db.search(RewardQuery.id == reward_id)
    if not reward_result:
        return jsonify({'error': 'Reward not found in reward database'}), 404

    reward = Reward.from_dict(reward_result[0])

    # Check if child has enough points
    logger.info(f'Child {child.name} has {child.points} points, reward {reward.name} costs {reward.cost} points')
    if child.points < reward.cost:
        points_needed = reward.cost - child.points
        return jsonify({
            'error': 'Insufficient points',
            'points_needed': points_needed,
            'current_points': child.points,
            'reward_cost': reward.cost
        }), 400

    # Check for duplicate pending request
    DupQuery = Query()
    duplicate = pending_confirmations_db.get(
        (DupQuery.child_id == child.id) & (DupQuery.entity_id == reward.id) &
        (DupQuery.entity_type == 'reward') & (DupQuery.status == 'pending') &
        (DupQuery.user_id == user_id)
    )
    if duplicate:
        today_local, tz_str = _get_user_today_local(user_id)
        if _is_epoch_timestamp_on_local_day(duplicate.get('created_at'), today_local, tz_str):
            return jsonify({'error': 'Reward request already pending', 'code': 'DUPLICATE_REWARD_REQUEST'}), 409
        pending_id = duplicate.get('id')
        if pending_id:
            pending_confirmations_db.remove(DupQuery.id == pending_id)

    pending = PendingConfirmation(child_id=child.id, entity_id=reward.id, entity_type='reward', user_id=user_id)
    pending_confirmations_db.insert(pending.to_dict())
    logger.info(f'Pending reward request created for child {child.name} for reward {reward.name}')
    
    # Create tracking event (no points change on request)
    tracking_event = TrackingEvent.create_event(
        user_id=user_id,
        child_id=child.id,
        entity_type='reward',
        entity_id=reward.id,
        action='requested',
        points_before=child.points,
        points_after=child.points,
        metadata={'reward_name': reward.name, 'reward_cost': reward.cost}
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)
    
    # Send tracking event via SSE
    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, 'reward', 'requested')))
    
    send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward.id, ChildRewardRequest.REQUEST_CREATED)))

    # Fire web push notification to all parent subscriptions
    _push_user = users_db.get(Query().id == user_id)
    if _push_user and _push_user.get('push_notifications_enabled', True):
        try:
            approve_token = create_action_token(user_id, child.id, reward.id, 'reward', 'approve')
            deny_token = create_action_token(user_id, child.id, reward.id, 'reward', 'deny')
            push_payload = {
                'type': 'reward_requested',
                'title': f'{child.name} wants a reward',
                'body': f'{reward.name} costs {reward.cost} points.',
                'user_id': user_id,
                'child_id': child.id,
                'child_name': child.name,
                'entity_id': reward.id,
                'entity_type': 'reward',
                'entity_name': reward.name,
                'approve_token': approve_token.id,
                'deny_token': deny_token.id,
            }
            send_push_to_user(user_id, push_payload)
        except Exception as _push_err:
            logger.warning(f'Push notification failed for reward request: {_push_err}')

    return jsonify({
        'message': f'Reward request for {reward.name} submitted for {child.name}.',
        'reward_id': reward.id,
        'reward_name': reward.name,
        'child_id': child.id,
        'child_name': child.name,
        'cost': reward.cost
    }), 200

@child_api.route('/child/<id>/cancel-request-reward', methods=['POST'])
def cancel_request_reward(id):
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    reward_id = data.get('reward_id')
    if not reward_id:
        return jsonify({'error': 'reward_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = Child.from_dict(result[0])

    # Fetch reward details for tracking metadata
    RewardQuery = Query()
    reward_result = reward_db.get((RewardQuery.id == reward_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
    reward_name = reward_result.get('name') if reward_result else 'Unknown'
    reward_cost = reward_result.get('cost', 0) if reward_result else 0

    # Remove matching pending reward request
    PendingQuery = Query()
    removed = pending_confirmations_db.remove(
        (PendingQuery.child_id == child.id) & (PendingQuery.entity_id == reward_id) &
        (PendingQuery.entity_type == 'reward') & (PendingQuery.user_id == user_id)
    )

    if not removed:
        return jsonify({'error': 'No pending request found for this reward'}), 404

    # Create tracking event (no points change on cancel)
    tracking_event = TrackingEvent.create_event(
        user_id=user_id,
        child_id=child.id,
        entity_type='reward',
        entity_id=reward_id,
        action='cancelled',
        points_before=child.points,
        points_after=child.points,
        metadata={'reward_name': reward_name, 'reward_cost': reward_cost}
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)
    
    # Send tracking event via SSE
    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value, TrackingEventCreated(tracking_event.id, child.id, 'reward', 'cancelled')))

    # Notify user that the request was cancelled
    resp = send_event_for_current_user(Event(EventType.CHILD_REWARD_REQUEST.value, ChildRewardRequest(child.id, reward_id, ChildRewardRequest.REQUEST_CANCELLED)))
    if resp:
        return resp
    return jsonify({
        'message': f'Reward request cancelled for {child.name}.',
        'child_id': child.id,
        'reward_id': reward_id,
        'removed_count': len(removed)
    }), 200



@child_api.route('/pending-confirmations', methods=['GET'])
def list_pending_confirmations():
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    PendingQuery = Query()
    today_local, tz_str = _get_user_today_local(user_id)
    pending_items = pending_confirmations_db.search(
        (PendingQuery.user_id == user_id) & (PendingQuery.status == 'pending')
    )
    responses = []

    RewardQuery = Query()
    TaskQuery = Query()
    RoutineQuery = Query()
    ChildQuery = Query()

    for pr in pending_items:
        pending = PendingConfirmation.from_dict(pr)

        if not _is_epoch_timestamp_on_local_day(pending.created_at, today_local, tz_str):
            pending_confirmations_db.remove(PendingQuery.id == pending.id)
            continue

        # Look up child details
        child_result = child_db.get(ChildQuery.id == pending.child_id)
        if not child_result:
            continue
        child = Child.from_dict(child_result)

        # Look up entity details based on type
        if pending.entity_type == 'reward':
            entity_result = reward_db.get((RewardQuery.id == pending.entity_id) & ((RewardQuery.user_id == user_id) | (RewardQuery.user_id == None)))
        elif pending.entity_type == 'chore':
            entity_result = task_db.get((TaskQuery.id == pending.entity_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
        elif pending.entity_type == 'routine':
            entity_result = routine_db.get((RoutineQuery.id == pending.entity_id) & ((RoutineQuery.user_id == user_id) | (RoutineQuery.user_id == None)))
        else:
            entity_result = None
        
        if not entity_result:
            continue

        response = PendingConfirmationResponse(
            _id=pending.id,
            child_id=child.id,
            child_name=child.name,
            child_image_id=child.image_id,
            entity_id=pending.entity_id,
            entity_type=pending.entity_type,
            entity_name=entity_result.get('name'),
            entity_image_id=entity_result.get('image_id'),
            status=pending.status,
            approved_at=pending.approved_at
        )
        responses.append(response.to_dict())

    return jsonify({'confirmations': responses, 'count': len(responses), 'list_type': 'notification'}), 200


# ---------------------------------------------------------------------------
# Chore Confirmation Endpoints
# ---------------------------------------------------------------------------

@child_api.route('/child/<id>/confirm-chore', methods=['POST'])
def confirm_chore(id):
    """Child confirms they completed a chore. Creates a pending confirmation."""
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    child = Child.from_dict(result[0])
    if task_id not in child.tasks:
        return jsonify({'error': 'Task not assigned to child', 'code': 'ENTITY_NOT_ASSIGNED'}), 400

    TaskQuery = Query()
    task_result = task_db.get((TaskQuery.id == task_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
    if not task_result:
        return jsonify({'error': 'Task not found', 'code': 'TASK_NOT_FOUND'}), 404

    task = Task.from_dict(task_result)
    if task.type != 'chore':
        return jsonify({'error': 'Only chores can be confirmed', 'code': 'INVALID_TASK_TYPE'}), 400

    # Check if already pending or completed today
    PendingQuery = Query()
    existing = pending_confirmations_db.get(
        (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
        (PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
    )
    if existing:
        today_local, tz_str = _get_user_today_local(user_id)
        if existing.get('status') == 'pending':
            if _is_epoch_timestamp_on_local_day(existing.get('created_at'), today_local, tz_str):
                return jsonify({'error': 'Chore already pending confirmation', 'code': 'CHORE_ALREADY_PENDING'}), 400
            pending_id = existing.get('id')
            if pending_id:
                pending_confirmations_db.remove(PendingQuery.id == pending_id)
        if existing.get('status') == 'approved':
            approved_at = existing.get('approved_at', '')
            if _is_iso_timestamp_on_local_day(approved_at, today_local, tz_str):
                return jsonify({'error': 'Chore already completed today', 'code': 'CHORE_ALREADY_COMPLETED'}), 400
            pending_id = existing.get('id')
            if pending_id:
                pending_confirmations_db.remove(PendingQuery.id == pending_id)

    confirmation = PendingConfirmation(
        child_id=id, entity_id=task_id, entity_type='chore', user_id=user_id
    )
    pending_confirmations_db.insert(confirmation.to_dict())

    # Create tracking event
    tracking_event = TrackingEvent.create_event(
        user_id=user_id, child_id=id, entity_type='chore', entity_id=task_id,
        action='confirmed', points_before=child.points, points_after=child.points,
        metadata={'task_name': task.name, 'task_type': task.type}
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)

    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value,
                                      TrackingEventCreated(tracking_event.id, id, 'chore', 'confirmed')))
    send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
                                      ChildChoreConfirmation(id, task_id, ChildChoreConfirmation.OPERATION_CONFIRMED)))

    # Fire web push notification to all parent subscriptions
    _push_user = users_db.get(Query().id == user_id)
    if _push_user and _push_user.get('push_notifications_enabled', True):
        try:
            approve_token = create_action_token(user_id, id, task_id, 'chore', 'approve')
            deny_token = create_action_token(user_id, id, task_id, 'chore', 'deny')
            push_payload = {
                'type': 'chore_confirmed',
                'title': f'{child.name} completed a chore',
                'body': f'{task.name} is waiting for your approval.',
                'user_id': user_id,
                'child_id': id,
                'child_name': child.name,
                'entity_id': task_id,
                'entity_type': 'chore',
                'entity_name': task.name,
                'approve_token': approve_token.id,
                'deny_token': deny_token.id,
            }
            send_push_to_user(user_id, push_payload)
        except Exception as _push_err:
            logger.warning(f'Push notification failed for chore confirmation: {_push_err}')

    return jsonify({'message': f'Chore {task.name} confirmed by {child.name}.', 'confirmation_id': confirmation.id}), 200


@child_api.route('/child/<id>/cancel-confirm-chore', methods=['POST'])
def cancel_confirm_chore(id):
    """Child cancels their pending chore confirmation."""
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    child = Child.from_dict(result[0])

    PendingQuery = Query()
    existing = pending_confirmations_db.get(
        (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
        (PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'pending') &
        (PendingQuery.user_id == user_id)
    )
    if not existing:
        return jsonify({'error': 'No pending confirmation found', 'code': 'PENDING_NOT_FOUND'}), 400

    pending_confirmations_db.remove(
        (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
        (PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'pending') &
        (PendingQuery.user_id == user_id)
    )

    # Fetch task name for tracking
    TaskQuery = Query()
    task_result = task_db.get((TaskQuery.id == task_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
    task_name = task_result.get('name') if task_result else 'Unknown'

    tracking_event = TrackingEvent.create_event(
        user_id=user_id, child_id=id, entity_type='chore', entity_id=task_id,
        action='cancelled', points_before=child.points, points_after=child.points,
        metadata={'task_name': task_name}
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)

    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value,
                                      TrackingEventCreated(tracking_event.id, id, 'chore', 'cancelled')))
    send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
                                      ChildChoreConfirmation(id, task_id, ChildChoreConfirmation.OPERATION_CANCELLED)))
    return jsonify({'message': 'Chore confirmation cancelled.'}), 200


@child_api.route('/child/<id>/approve-chore', methods=['POST'])
def approve_chore(id):
    """Parent approves a pending chore confirmation, awarding points."""
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    try:
        result = chore_actions.approve_chore(user_id, id, task_id)
    except ValueError:
        return jsonify({'error': 'Child or task not found'}), 404

    if result is None:
        return jsonify({'error': 'No pending confirmation found', 'code': 'PENDING_NOT_FOUND'}), 400

    return jsonify({
        'message': f'Chore {result["task_name"]} approved for {result["child_name"]}.',
        'points': result['points'],
        'id': result['child_id']
    }), 200


@child_api.route('/child/<id>/reject-chore', methods=['POST'])
def reject_chore(id):
    """Parent rejects a pending chore confirmation."""
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404

    PendingQuery = Query()
    existing = pending_confirmations_db.get(
        (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
        (PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'pending') &
        (PendingQuery.user_id == user_id)
    )
    if not existing:
        return jsonify({'error': 'No pending confirmation found', 'code': 'PENDING_NOT_FOUND'}), 400

    chore_actions.reject_chore(user_id, id, task_id)
    return jsonify({'message': 'Chore confirmation rejected.'}), 200


@child_api.route('/child/<id>/reset-chore', methods=['POST'])
def reset_chore(id):
    """Parent resets a completed chore so the child can confirm again."""
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    task_id = data.get('task_id')
    if not task_id:
        return jsonify({'error': 'task_id is required'}), 400

    ChildQuery = Query()
    result = child_db.search((ChildQuery.id == id) & (ChildQuery.user_id == user_id))
    if not result:
        return jsonify({'error': 'Child not found'}), 404
    child = Child.from_dict(result[0])

    PendingQuery = Query()
    existing = pending_confirmations_db.get(
        (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
        (PendingQuery.entity_type == 'chore') & (PendingQuery.status == 'approved') &
        (PendingQuery.user_id == user_id)
    )
    if not existing:
        return jsonify({'error': 'No completed confirmation found to reset', 'code': 'PENDING_NOT_FOUND'}), 400

    pending_confirmations_db.remove(
        (PendingQuery.child_id == id) & (PendingQuery.entity_id == task_id) &
        (PendingQuery.entity_type == 'chore') & (PendingQuery.user_id == user_id)
    )

    TaskQuery = Query()
    task_result = task_db.get((TaskQuery.id == task_id) & ((TaskQuery.user_id == user_id) | (TaskQuery.user_id == None)))
    task_name = task_result.get('name') if task_result else 'Unknown'

    tracking_event = TrackingEvent.create_event(
        user_id=user_id, child_id=id, entity_type='chore', entity_id=task_id,
        action='reset', points_before=child.points, points_after=child.points,
        metadata={'task_name': task_name}
    )
    insert_tracking_event(tracking_event)
    log_tracking_event(tracking_event)

    send_event_for_current_user(Event(EventType.TRACKING_EVENT_CREATED.value,
                                      TrackingEventCreated(tracking_event.id, id, 'chore', 'reset')))
    send_event_for_current_user(Event(EventType.CHILD_CHORE_CONFIRMATION.value,
                                      ChildChoreConfirmation(id, task_id, ChildChoreConfirmation.OPERATION_RESET)))
    return jsonify({'message': 'Chore reset to available.'}), 200


@child_api.route('/child/<id>/deny-reward-request', methods=['POST'])
def deny_reward_request(id):
    """Parent denies a child's pending reward request."""
    user_id = get_validated_user_id()
    if not user_id:
        return jsonify({'error': 'Unauthorized', 'code': 'UNAUTHORIZED'}), 401
    data = request.get_json()
    reward_id = data.get('reward_id')
    if not reward_id:
        return jsonify({'error': 'reward_id is required'}), 400

    result = chore_actions.deny_reward(user_id, id, reward_id)
    if result is None:
        return jsonify({'message': 'This reward request has already been resolved.'}), 200

    return jsonify({'message': f'Reward request denied for {result["child_name"]}.'}), 200


