"""Estimates blueprint — estimate-to-close funnel tracking for remodeling sector.

Provides CRUD for estimates, stage transition management, and funnel analytics
(conversion rates, time-in-stage, drop-off points, pipeline value).

All routes are company-scoped and require authentication.
"""
import re
import uuid
from datetime import datetime, timedelta, timezone
from flask import Blueprint, request, jsonify
from flask_login import current_user
from app.routes.api_proxy import (
    require_auth_json,
    require_csrf,
    _check_company_access,
)
from app.utils.pagination import paginate_query
from app.models import db, Estimate

estimates_bp = Blueprint('estimates_api', __name__)

# ============================================================================
# Helper functions
# ============================================================================

def _generate_estimate_number(company_id):
    """Generate a unique estimate number for a company.

    Format: EST-{YYYYMMDD}-{SEQ} where SEQ is a short random suffix.
    Retries on collision to guarantee uniqueness.
    """
    for _ in range(5):
        date_str = datetime.now(timezone.utc).strftime('%Y%m%d')
        suffix = uuid.uuid4().hex[:6].upper()
        number = f"EST-{date_str}-{suffix}"
        exists = Estimate.query.filter_by(company_id=company_id, estimate_number=number).first()
        if not exists:
            return number
    # Fallback: add timestamp microseconds
    ts = datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S%f')[:14]
    return f"EST-{ts}"


def _estimate_to_dict(estimate):
    """Convert Estimate model to serializable dict."""
    return estimate.to_dict()


# ============================================================================
# CRUD Routes
# ============================================================================

@estimates_bp.route('/api/company/<company_id>/estimates', methods=['GET'])
@require_auth_json()
def list_estimates(company_id):
    """List estimates for a company with optional filtering.

    Query params:
    - stage: Filter by stage (draft, delivered, accepted, rejected, expired, scheduled, closed)
    - source: Filter by source (manual, angi, servicetitan, jobber, etc.)
    - min_value: Filter by minimum total_value
    - max_value: Filter by maximum total_value
    - project_type: Filter by project type
    - is_active: Filter by active status (true/false)
    - is_closed: Filter by closed status (true/false)
    - page, per_page: Pagination params
    """
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    query = Estimate.query.filter_by(company_id=company_id)

    # Filters
    stage_filter = request.args.get('stage')
    if stage_filter:
        query = query.filter_by(stage=stage_filter)

    source_filter = request.args.get('source')
    if source_filter:
        query = query.filter_by(source=source_filter)

    min_value = request.args.get('min_value', type=float)
    if min_value is not None:
        query = query.filter(Estimate.total_value >= min_value)

    max_value = request.args.get('max_value', type=float)
    if max_value is not None:
        query = query.filter(Estimate.total_value <= max_value)

    project_type_filter = request.args.get('project_type')
    if project_type_filter:
        query = query.filter(Estimate.project_type.ilike(f'%{project_type_filter}%'))

    is_active = request.args.get('is_active')
    if is_active is not None:
        if is_active.lower() == 'true':
            query = query.filter(Estimate.stage.in_(['draft', 'delivered', 'accepted', 'scheduled']))
        else:
            query = query.filter(Estimate.stage.in_(['rejected', 'expired', 'closed']))

    is_closed = request.args.get('is_closed')
    if is_closed is not None:
        if is_closed.lower() == 'true':
            query = query.filter(Estimate.stage.in_(['rejected', 'expired', 'closed']))
        else:
            query = query.filter(Estimate.stage.in_(['draft', 'delivered', 'accepted', 'scheduled']))

    # Sort by created_at desc by default
    sort_by = request.args.get('sort_by', 'created_at')
    sort_order = request.args.get('sort_order', 'desc')
    valid_sort_fields = ['created_at', 'total_value', 'stage', 'updated_at']
    if sort_by not in valid_sort_fields:
        sort_by = 'created_at'

    sort_column = getattr(Estimate, sort_by, Estimate.created_at)
    if sort_order == 'desc':
        query = query.order_by(sort_column.desc())
    else:
        query = query.order_by(sort_column.asc())

    pagination, meta = paginate_query(query)
    estimates = pagination.items

    return jsonify({
        **meta,
        'estimates': [_estimate_to_dict(e) for e in estimates],
    })


@estimates_bp.route('/api/company/<company_id>/estimates', methods=['POST'])
@require_auth_json()
@require_csrf
def create_estimate(company_id):
    """Create a new estimate.

    Required fields: total_value
    Optional fields: customer_name, customer_email, customer_phone,
    project_type, description, address, city, state, zip_code,
    deposit, tax_amount, discount_amount, line_items, project_id,
    customer_id, source_lead_id, expires_at, notes, assigned_to,
    source, source_estimate_id, stage
    """
    error_response, membership = _check_company_access(company_id, min_role='editor')
    if error_response:
        return error_response

    data = request.get_json()
    if not data:
        return jsonify({'error': 'Invalid request'}), 400

    total_value = data.get('total_value')
    if total_value is None:
        return jsonify({'error': 'total_value is required'}), 400

    try:
        total_value = float(total_value)
    except (ValueError, TypeError):
        return jsonify({'error': 'total_value must be a number'}), 400

    # Validate stage if provided
    stage = data.get('stage', 'draft')
    valid_stages = ['draft', 'delivered', 'accepted', 'rejected', 'expired', 'scheduled', 'closed']
    if stage not in valid_stages:
        return jsonify({'error': f'stage must be one of: {", ".join(valid_stages)}'}), 400

    # Validate source if provided
    source = data.get('source', 'manual')
    valid_sources = ['manual', 'angi', 'servicetitan', 'jobber', 'hubspot', 'email', 'referral', 'website']
    if source not in valid_sources:
        return jsonify({'error': f'source must be one of: {", ".join(valid_sources)}'}), 400

    # Parse line_items
    line_items = data.get('line_items', [])
    if isinstance(line_items, str):
        import json
        try:
            line_items = json.loads(line_items)
        except json.JSONDecodeError:
            return jsonify({'error': 'line_items must be valid JSON'}), 400

    # Parse expires_at
    expires_at = None
    if data.get('expires_at'):
        try:
            expires_at = datetime.fromisoformat(data['expires_at'])
        except (ValueError, TypeError):
            return jsonify({'error': 'expires_at must be a valid ISO datetime'}), 400

    # Build estimate
    estimate = Estimate(
        company_id=company_id,
        estimate_number=data.get('estimate_number') or _generate_estimate_number(company_id),
        project_id=data.get('project_id'),
        customer_id=data.get('customer_id'),
        source_lead_id=data.get('source_lead_id'),
        customer_name=data.get('customer_name', '').strip(),
        customer_email=data.get('customer_email', '').strip(),
        customer_phone=data.get('customer_phone', '').strip(),
        project_type=data.get('project_type', '').strip(),
        description=data.get('description', '').strip(),
        address=data.get('address', '').strip(),
        city=data.get('city', '').strip(),
        state=data.get('state', '').strip(),
        zip_code=data.get('zip_code', '').strip(),
        total_value=total_value,
        deposit=float(data.get('deposit', 0.0)),
        tax_amount=float(data.get('tax_amount', 0.0)),
        discount_amount=float(data.get('discount_amount', 0.0)),
        line_items_json=line_items,
        stage=stage,
        expires_at=expires_at,
        rejection_reason=data.get('rejection_reason', '').strip(),
        competitor_name=data.get('competitor_name', '').strip(),
        competitor_price=data.get('competitor_price'),
        notes=data.get('notes', '').strip(),
        assigned_to=data.get('assigned_to'),
        source=source,
        source_estimate_id=data.get('source_estimate_id', '').strip(),
    )

    # Initialize stage_history with creation record
    now = datetime.now(timezone.utc)
    estimate.stage_history = [{
        'from_stage': None,
        'to_stage': stage,
        'timestamp': now.isoformat(),
        'notes': 'Estimate created',
    }]

    # Set stage-specific timestamps if created in non-draft stage
    if stage == 'delivered':
        estimate.delivered_at = now
    elif stage == 'accepted':
        estimate.accepted_at = now
    elif stage == 'rejected':
        estimate.rejected_at = now

    db.session.add(estimate)
    db.session.commit()

    return jsonify({
        'success': True,
        'estimate': _estimate_to_dict(estimate),
    }), 201


@estimates_bp.route('/api/company/<company_id>/estimates/<estimate_id>', methods=['GET'])
@require_auth_json()
def get_estimate(company_id, estimate_id):
    """Get a single estimate by ID."""
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    estimate = db.session.get(Estimate, estimate_id)
    if not estimate or estimate.company_id != company_id:
        return jsonify({'error': 'Estimate not found'}), 404

    return jsonify({
        'estimate': _estimate_to_dict(estimate),
    })


@estimates_bp.route('/api/company/<company_id>/estimates/<estimate_id>', methods=['PUT'])
@require_auth_json()
@require_csrf
def update_estimate(company_id, estimate_id):
    """Update an estimate.

    All fields are optional. Only provided fields will be updated.
    Stage changes should use the transition endpoint instead.
    """
    error_response, membership = _check_company_access(company_id, min_role='editor')
    if error_response:
        return error_response

    estimate = db.session.get(Estimate, estimate_id)
    if not estimate or estimate.company_id != company_id:
        return jsonify({'error': 'Estimate not found'}), 404

    data = request.get_json()
    if not data:
        return jsonify({'error': 'Invalid request'}), 400

    # Updateable fields
    updatable_fields = [
        'customer_name', 'customer_email', 'customer_phone',
        'project_type', 'description',
        'address', 'city', 'state', 'zip_code',
        'total_value', 'deposit', 'tax_amount', 'discount_amount',
        'notes', 'assigned_to', 'expires_at',
        'rejection_reason', 'competitor_name', 'competitor_price',
        'project_id', 'customer_id', 'source_lead_id',
        'source', 'source_estimate_id',
    ]

    for field in updatable_fields:
        if field in data:
            value = data[field]
            # Handle datetime fields
            if field == 'expires_at' and value:
                try:
                    value = datetime.fromisoformat(value)
                except (ValueError, TypeError):
                    return jsonify({'error': f'{field} must be a valid ISO datetime'}), 400
            # Handle numeric fields
            if field in ('total_value', 'deposit', 'tax_amount', 'discount_amount', 'competitor_price'):
                if value is not None:
                    try:
                        value = float(value)
                    except (ValueError, TypeError):
                        return jsonify({'error': f'{field} must be a number'}), 400
            setattr(estimate, field, value)

    # Handle line_items separately
    if 'line_items' in data:
        line_items = data['line_items']
        if isinstance(line_items, str):
            import json
            try:
                line_items = json.loads(line_items)
            except json.JSONDecodeError:
                return jsonify({'error': 'line_items must be valid JSON'}), 400
        estimate.line_items_json = line_items

    db.session.commit()

    return jsonify({
        'success': True,
        'estimate': _estimate_to_dict(estimate),
    })


@estimates_bp.route('/api/company/<company_id>/estimates/<estimate_id>', methods=['DELETE'])
@require_auth_json()
@require_csrf
def delete_estimate(company_id, estimate_id):
    """Delete an estimate."""
    error_response, membership = _check_company_access(company_id, min_role='editor')
    if error_response:
        return error_response

    estimate = db.session.get(Estimate, estimate_id)
    if not estimate or estimate.company_id != company_id:
        return jsonify({'error': 'Estimate not found'}), 404

    db.session.delete(estimate)
    db.session.commit()

    return jsonify({
        'success': True,
        'message': 'Estimate deleted',
    })


# ============================================================================
# Stage Transitions
# ============================================================================

@estimates_bp.route('/api/company/<company_id>/estimates/<estimate_id>/transition', methods=['PATCH'])
@require_auth_json()
@require_csrf
def transition_estimate(company_id, estimate_id):
    """Transition an estimate to a new stage with audit trail.

    Valid transitions:
    - draft → delivered
    - delivered → accepted, rejected, expired
    - accepted → scheduled, rejected
    - scheduled → closed, rejected
    - rejected, expired, closed are terminal stages

    Body: { "stage": "delivered", "notes": "Optional notes" }
    """
    error_response, membership = _check_company_access(company_id, min_role='editor')
    if error_response:
        return error_response

    estimate = db.session.get(Estimate, estimate_id)
    if not estimate or estimate.company_id != company_id:
        return jsonify({'error': 'Estimate not found'}), 404

    data = request.get_json()
    if not data:
        return jsonify({'error': 'Invalid request'}), 400

    new_stage = data.get('stage')
    if not new_stage:
        return jsonify({'error': 'stage is required'}), 400

    notes = data.get('notes', '')

    # Validate new stage
    valid_stages = ['draft', 'delivered', 'accepted', 'rejected', 'expired', 'scheduled', 'closed']
    if new_stage not in valid_stages:
        return jsonify({'error': f'stage must be one of: {", ".join(valid_stages)}'}), 400

    # Attempt transition
    success = estimate.transition_to(new_stage, notes=notes)

    if not success:
        return jsonify({
            'error': f'Invalid transition from {estimate.stage} to {new_stage}',
            'current_stage': estimate.stage,
            'requested_stage': new_stage,
        }), 400

    # Handle additional transition data
    if new_stage == 'scheduled' and data.get('scheduled_date'):
        try:
            estimate.scheduled_date = datetime.fromisoformat(data['scheduled_date'])
        except (ValueError, TypeError):
            return jsonify({'error': 'scheduled_date must be a valid ISO datetime'}), 400

    db.session.commit()

    return jsonify({
        'success': True,
        'estimate': _estimate_to_dict(estimate),
        'transition': {
            'from_stage': data.get('previous_stage', estimate.stage),
            'to_stage': new_stage,
            'timestamp': datetime.now(timezone.utc).isoformat(),
        },
    })


@estimates_bp.route('/api/company/<company_id>/estimates/<estimate_id>/stage-history', methods=['GET'])
@require_auth_json()
def get_stage_history(company_id, estimate_id):
    """Get the stage transition history for an estimate."""
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    estimate = db.session.get(Estimate, estimate_id)
    if not estimate or estimate.company_id != company_id:
        return jsonify({'error': 'Estimate not found'}), 404

    return jsonify({
        'estimate_id': estimate_id,
        'current_stage': estimate.stage,
        'stage_history': estimate.stage_history or [],
    })


# ============================================================================
# Bulk Operations
# ============================================================================

@estimates_bp.route('/api/company/<company_id>/estimates/expired', methods=['PATCH'])
@require_auth_json()
@require_csrf
def expire_old_estimates(company_id):
    """Mark all delivered estimates past their expiration date as expired.

    Optional query param: days_before_expiry — also expire estimates
    that will expire within N days (for proactive cleanup).
    """
    error_response, membership = _check_company_access(company_id, min_role='editor')
    if error_response:
        return error_response

    now = datetime.now(timezone.utc)
    days_ahead = request.args.get('days_before_expiry', type=int) or 0

    cutoff = now - timedelta(days=0)
    if days_ahead > 0:
        cutoff = now + timedelta(days=days_ahead)

    # Find delivered/accepted estimates past expiry
    query = Estimate.query.filter(
        Estimate.company_id == company_id,
        Estimate.stage.in_(['delivered', 'accepted', 'scheduled']),
        Estimate.expires_at.isnot(None),
        Estimate.expires_at <= cutoff,
    )

    expired = query.all()
    count = 0

    for estimate in expired:
        estimate.transition_to('expired', notes=f'Auto-expired on {now.strftime("%Y-%m-%d")}')
        count += 1

    db.session.commit()

    return jsonify({
        'success': True,
        'expired_count': count,
        'expired_ids': [e.id for e in expired],
    })


# ============================================================================
# Funnel Analytics
# ============================================================================

@estimates_bp.route('/api/company/<company_id>/estimates/funnel', methods=['GET'])
@require_auth_json()
def get_funnel_analytics(company_id):
    """Get funnel analytics for a company.

    Returns:
    - stage_counts: Count of estimates per stage
    - stage_values: Total value of estimates per stage
    - conversion_rates: Conversion rate between consecutive stages
    - avg_time_in_stage: Average days per stage
    - pipeline_summary: Total pipeline value, win rate, average deal size
    - drop_off_points: Stages with highest drop-off rates

    Query params:
    - days: Filter by date range (default: 365)
    - source: Filter by source
    - project_type: Filter by project type
    """
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    days = request.args.get('days', 365, type=int)
    source_filter = request.args.get('source')
    project_type_filter = request.args.get('project_type')

    since = datetime.now(timezone.utc) - timedelta(days=days)

    query = Estimate.query.filter(
        Estimate.company_id == company_id,
        Estimate.created_at >= since,
    )

    if source_filter:
        query = query.filter_by(source=source_filter)

    if project_type_filter:
        query = query.filter(Estimate.project_type.ilike(f'%{project_type_filter}%'))

    estimates = query.all()

    # Stage counts and values
    stage_counts = {}
    stage_values = {}
    stage_entry_times = {}  # For avg time calculation

    all_stages = ['draft', 'delivered', 'accepted', 'rejected', 'expired', 'scheduled', 'closed']
    for stage in all_stages:
        stage_counts[stage] = 0
        stage_values[stage] = 0.0

    for e in estimates:
        stage_counts[e.stage] = stage_counts.get(e.stage, 0) + 1
        stage_values[e.stage] = stage_values.get(e.stage, 0.0) + (e.total_value or 0)

    # Conversion rates
    # Funnel flow: draft → delivered → accepted → scheduled → closed
    funnel_stages = ['draft', 'delivered', 'accepted', 'scheduled', 'closed']
    conversion_rates = {}
    for i, stage in enumerate(funnel_stages[:-1]):
        current_count = stage_counts.get(stage, 0)
        next_stage = funnel_stages[i + 1]
        next_count = stage_counts.get(next_stage, 0)
        if current_count > 0:
            rate = min(round((next_count / current_count) * 100, 2), 100.0)
            conversion_rates[f'{stage}_to_{next_stage}'] = {
                'from_stage': stage,
                'to_stage': next_stage,
                'from_count': current_count,
                'to_count': next_count,
                'rate': rate,
            }
        else:
            conversion_rates[f'{stage}_to_{next_stage}'] = {
                'from_stage': stage,
                'to_stage': next_stage,
                'from_count': 0,
                'to_count': 0,
                'rate': 0,
            }

    # Overall win rate (closed / total)
    total_count = len(estimates)
    closed_count = stage_counts.get('closed', 0)
    win_rate = round((closed_count / total_count) * 100, 2) if total_count > 0 else 0

    # Average time in stage (for delivered estimates with acceptance)
    times_to_accept = []
    for e in estimates:
        if e.delivered_at and e.accepted_at:
            days_to_accept = (e.accepted_at - e.delivered_at).days
            times_to_accept.append(days_to_accept)

    avg_time_to_accept = round(sum(times_to_accept) / len(times_to_accept), 1) if times_to_accept else None

    # Pipeline summary
    pipeline_value = sum(e.total_value for e in estimates if e.is_active)
    closed_value = sum(e.total_value for e in estimates if e.stage == 'closed')
    avg_deal_size = closed_value / closed_count if closed_count > 0 else 0

    # Drop-off analysis
    rejected_count = stage_counts.get('rejected', 0)
    expired_count = stage_counts.get('expired', 0)
    drop_off_rate = round(((rejected_count + expired_count) / total_count) * 100, 2) if total_count > 0 else 0

    # Rejection reasons breakdown
    rejection_reasons = {}
    for e in estimates:
        if e.stage == 'rejected' and e.rejection_reason:
            reason = e.rejection_reason
            rejection_reasons[reason] = rejection_reasons.get(reason, 0) + 1

    return jsonify({
        'period_days': days,
        'total_estimates': total_count,
        'stage_counts': stage_counts,
        'stage_values': stage_values,
        'conversion_rates': conversion_rates,
        'pipeline_summary': {
            'total_pipeline_value': pipeline_value,
            'closed_value': closed_value,
            'closed_count': closed_count,
            'win_rate': win_rate,
            'avg_deal_size': round(avg_deal_size, 2),
            'avg_time_to_accept_days': avg_time_to_accept,
        },
        'drop_off': {
            'rejected_count': rejected_count,
            'expired_count': expired_count,
            'total_drop_off_count': rejected_count + expired_count,
            'drop_off_rate': drop_off_rate,
            'rejection_reasons': rejection_reasons,
        },
    })


@estimates_bp.route('/api/company/<company_id>/estimates/pipeline', methods=['GET'])
@require_auth_json()
def get_pipeline_overview(company_id):
    """Get a high-level pipeline overview for dashboard display.

    Returns stage breakdown with counts and values, plus key metrics.
    """
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    days = request.args.get('days', 90, type=int)
    since = datetime.now(timezone.utc) - timedelta(days=days)

    estimates = Estimate.query.filter(
        Estimate.company_id == company_id,
        Estimate.created_at >= since,
    ).all()

    # Build pipeline stages
    pipeline = {}
    for stage in ['draft', 'delivered', 'accepted', 'scheduled', 'closed']:
        stage_estimates = [e for e in estimates if e.stage == stage]
        pipeline[stage] = {
            'count': len(stage_estimates),
            'value': sum(e.total_value for e in stage_estimates),
            'avg_value': round(sum(e.total_value for e in stage_estimates) / len(stage_estimates), 2) if stage_estimates else 0,
        }

    # Key metrics
    total = len(estimates)
    active = [e for e in estimates if e.is_active]
    closed = [e for e in estimates if e.stage == 'closed']

    return jsonify({
        'period_days': days,
        'total_estimates': total,
        'active_estimates': len(active),
        'closed_estimates': len(closed),
        'pipeline': pipeline,
        'total_pipeline_value': sum(e.total_value for e in active),
        'total_closed_value': sum(e.total_value for e in closed),
    })


@estimates_bp.route('/api/company/<company_id>/estimates/stats', methods=['GET'])
@require_auth_json()
def get_estimate_stats(company_id):
    """Get quick estimate statistics for dashboard widgets.

    Returns:
    - total_count: Total estimates in period
    - active_count: Estimates in active stages
    - closed_count: Estimates that are closed
    - total_value: Sum of all estimate values
    - pipeline_value: Sum of active estimate values
    - closed_value: Sum of closed estimate values
    - avg_estimate_size: Average estimate value
    - conversion_rate: Percentage that reached closed stage
    - avg_days_to_close: Average days from creation to close
    """
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    days = request.args.get('days', 30, type=int)
    since = datetime.now(timezone.utc) - timedelta(days=days)

    estimates = Estimate.query.filter(
        Estimate.company_id == company_id,
        Estimate.created_at >= since,
    ).all()

    total = len(estimates)
    active = [e for e in estimates if e.is_active]
    closed = [e for e in estimates if e.stage == 'closed']

    # Average days to close
    days_to_close = []
    for e in closed:
        if e.created_at and e.closed_at:
            days_to_close.append((e.closed_at - e.created_at).days)

    avg_days_to_close = round(sum(days_to_close) / len(days_to_close), 1) if days_to_close else None

    total_value = sum(e.total_value for e in estimates)
    pipeline_value = sum(e.total_value for e in active)
    closed_value = sum(e.total_value for e in closed)

    return jsonify({
        'period_days': days,
        'total_count': total,
        'active_count': len(active),
        'closed_count': len(closed),
        'total_value': total_value,
        'pipeline_value': pipeline_value,
        'closed_value': closed_value,
        'avg_estimate_size': round(total_value / total, 2) if total > 0 else 0,
        'conversion_rate': round((len(closed) / total) * 100, 2) if total > 0 else 0,
        'avg_days_to_close': avg_days_to_close,
    })


@estimates_bp.route('/api/company/<company_id>/estimates/by-source', methods=['GET'])
@require_auth_json()
def get_estimates_by_source(company_id):
    """Get estimate breakdown by source channel.

    Returns per-source stats including count, value, and conversion rate.
    """
    error_response, membership = _check_company_access(company_id, min_role='member')
    if error_response:
        return error_response

    days = request.args.get('days', 90, type=int)
    since = datetime.now(timezone.utc) - timedelta(days=days)

    estimates = Estimate.query.filter(
        Estimate.company_id == company_id,
        Estimate.created_at >= since,
    ).all()

    sources = {}
    for e in estimates:
        source = e.source or 'unknown'
        if source not in sources:
            sources[source] = {
                'count': 0,
                'total_value': 0.0,
                'closed_count': 0,
                'closed_value': 0.0,
            }
        sources[source]['count'] += 1
        sources[source]['total_value'] += e.total_value or 0
        if e.stage == 'closed':
            sources[source]['closed_count'] += 1
            sources[source]['closed_value'] += e.total_value or 0

    # Add conversion rates
    for source, data in sources.items():
        data['conversion_rate'] = round((data['closed_count'] / data['count']) * 100, 2) if data['count'] > 0 else 0
        data['avg_value'] = round(data['total_value'] / data['count'], 2) if data['count'] > 0 else 0

    return jsonify({
        'period_days': days,
        'sources': sources,
    })


# ============================================================================
# Estimate-Project Linking
# ============================================================================

@estimates_bp.route('/api/company/<company_id>/estimates/<estimate_id>/link-project', methods=['PATCH'])
@require_auth_json()
@require_csrf
def link_estimate_to_project(company_id, estimate_id):
    """Link an accepted estimate to an existing project.

    Body: { "project_id": "uuid" }
    """
    error_response, membership = _check_company_access(company_id, min_role='editor')
    if error_response:
        return error_response

    estimate = db.session.get(Estimate, estimate_id)
    if not estimate or estimate.company_id != company_id:
        return jsonify({'error': 'Estimate not found'}), 404

    data = request.get_json()
    if not data or not data.get('project_id'):
        return jsonify({'error': 'project_id is required'}), 400

    from app.models import Project
    project = db.session.get(Project, data['project_id'])
    if not project or project.company_id != company_id:
        return jsonify({'error': 'Project not found'}), 404

    estimate.project_id = data['project_id']
    db.session.commit()

    return jsonify({
        'success': True,
        'estimate': _estimate_to_dict(estimate),
        'project_id': project.id,
        'project_name': project.name,
    })
