"""
SMS/Call connector API routes — unified endpoints for the frontend.

Provides timeline, messages, calls, metrics, and outbound actions
for the SMS + Call connector (Twilio-backed).

These routes are registered under /app/api/sms-call and are intended
for the SPA's smsCall.ts API layer.
"""
import logging
from datetime import datetime, timezone, timedelta
from functools import wraps

from flask import Blueprint, request, jsonify, current_app
from flask_login import current_user
from sqlalchemy import select, func, and_, or_

from app import db
from app.models import (
    SmsMessage,
    Call,
    LeadTouchpoint,
    OptOut,
    AngiLead,
    Connector,
)
from app.connectors import build_connector, ConnectorError
from app.routes.api_proxy import require_auth_json
from app.utils.csrf import require_csrf

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Blueprint
# ---------------------------------------------------------------------------

sms_call_bp = Blueprint('sms_call', __name__)


def _current_company_id():
    """Resolve the current user's company_id via UserCompany membership.

    User has NO ``company_id`` column. Honors an optional ``?company_id=``
    query param (membership-checked), else falls back to the user's first
    company membership. Aborts 403 if the user has no company access.
    """
    from flask import abort
    from app.models import UserCompany
    from app.utils.tenancy import resolve_company_id

    membership = UserCompany.query.filter_by(user_id=current_user.id).first()
    default_id = membership.company_id if membership else None
    company_id = resolve_company_id(default=default_id)
    if not company_id:
        abort(403, description='No company access')
    return company_id


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _get_sms_connector(company_id: str):
    """Look up and build the active SMS connector for a company."""
    connector = Connector.query.filter_by(
        company_id=company_id,
        service='sms',
        active=True,
    ).first()
    if not connector:
        raise ConnectorError('No active SMS connector configured for this company')
    return build_connector('sms', company_id, connector.config, connector.id)


def _pagination_params(default_limit: int = 50, max_limit: int = 200):
    """Extract limit/offset from query string with clamping."""
    limit = min(int(request.args.get('limit', default_limit)), max_limit)
    offset = int(request.args.get('offset', 0))
    return limit, offset


# ---------------------------------------------------------------------------
# Timeline — GET /timeline?lead_id=XXX
# ---------------------------------------------------------------------------

@sms_call_bp.route('/timeline', methods=['GET'])
@require_auth_json()
def timeline():
    """Return unified chronological touchpoints for a lead.

    Response matches TimelineResponse:
        { lead_id, lead_status, touchpoints: LeadTouchpoint[], total }
    """
    lead_id = request.args.get('lead_id')
    if not lead_id:
        return jsonify({'error': 'lead_id query parameter is required'}), 400

    company_id = _current_company_id()

    # Verify lead belongs to company
    lead = db.session.get(AngiLead, lead_id)
    if not lead or lead.company_id != company_id:
        return jsonify({'error': 'Lead not found'}), 404

    touchpoints = db.session.execute(
        select(LeadTouchpoint).where(
            and_(
                LeadTouchpoint.company_id == company_id,
                LeadTouchpoint.lead_id == lead_id,
            )
        ).order_by(LeadTouchpoint.created_at)
    ).scalars().all()

    return jsonify({
        'lead_id': lead_id,
        'lead_status': lead.status,
        'touchpoints': [tp.to_dict() for tp in touchpoints],
        'total': len(touchpoints),
    })


# ---------------------------------------------------------------------------
# Messages — GET /messages?lead_id=XXX
# ---------------------------------------------------------------------------

@sms_call_bp.route('/messages', methods=['GET'])
@require_auth_json()
def messages():
    """List SMS messages with optional filters.

    Response matches MessagesResponse:
        { messages: SmsMessage[], total, limit, offset }
    """
    company_id = _current_company_id()
    lead_id = request.args.get('lead_id')
    direction = request.args.get('direction')
    status = request.args.get('status')
    phone = request.args.get('phone')
    limit, offset = _pagination_params(default_limit=50)

    query = select(SmsMessage).where(SmsMessage.company_id == company_id)

    if lead_id:
        query = query.where(SmsMessage.lead_id == lead_id)
    if direction:
        query = query.where(SmsMessage.direction == direction)
    if status:
        query = query.where(SmsMessage.status == status)
    if phone:
        query = query.where(
            or_(
                SmsMessage.from_phone == phone,
                SmsMessage.to_phone == phone,
            )
        )

    query = query.order_by(SmsMessage.created_at.desc()).offset(offset).limit(limit)
    results = db.session.execute(query).scalars().all()

    # Total count for pagination
    count_query = select(func.count()).select_from(SmsMessage).where(
        SmsMessage.company_id == company_id
    )
    if lead_id:
        count_query = count_query.where(SmsMessage.lead_id == lead_id)

    total = db.session.execute(count_query).scalar_one()

    return jsonify({
        'messages': [m.to_dict() for m in results],
        'total': total,
        'limit': limit,
        'offset': offset,
    })


# ---------------------------------------------------------------------------
# Send SMS — POST /send
# ---------------------------------------------------------------------------

@sms_call_bp.route('/send', methods=['POST'])
@require_auth_json()
@require_csrf
def send():
    """Send outbound SMS with opt-out suppression check.

    Request body (SendSmsPayload): { to_phone, body, lead_id? }
    Response matches SendSmsResponse:
        { success, sms_id, status, provider_sid, to_phone, body }
    """
    data = request.get_json()
    if not data:
        return jsonify({'error': 'JSON body required'}), 400

    company_id = _current_company_id()
    to_phone = data.get('to_phone', '').strip()
    body = data.get('body', '').strip()
    lead_id = data.get('lead_id')

    if not to_phone or not body:
        return jsonify({'error': 'to_phone and body are required'}), 400

    if len(body) > 1600:
        return jsonify({'error': 'Message body exceeds 1600 characters'}), 400

    try:
        connector = _get_sms_connector(company_id)
        result = connector.send_sms(
            company_id=company_id,
            to_phone=to_phone,
            body=body,
            lead_id=lead_id,
        )
        return jsonify({
            'success': True,
            'sms_id': result['sms_id'],
            'status': result['status'],
            'provider_sid': result['provider_sid'],
            'to_phone': result['to_phone'],
            'body': result['body'],
        })
    except ConnectorError as e:
        logger.error(f'SMS send connector error: {e}')
        return jsonify({'error': 'SMS provider error'}), 400
    except Exception as e:
        logger.error(f'SMS send failed: {e}')
        return jsonify({'error': 'Failed to send SMS'}), 500


# ---------------------------------------------------------------------------
# Calls — GET /calls?lead_id=XXX
# ---------------------------------------------------------------------------

@sms_call_bp.route('/calls', methods=['GET'])
@require_auth_json()
def calls():
    """List calls with optional filters.

    Response matches CallsResponse:
        { calls: CallRecord[], total, limit, offset }
    """
    company_id = _current_company_id()
    lead_id = request.args.get('lead_id')
    disposition = request.args.get('disposition')
    limit, offset = _pagination_params(default_limit=50)

    query = select(Call).where(Call.company_id == company_id)

    if lead_id:
        query = query.where(Call.lead_id == lead_id)
    if disposition:
        query = query.where(Call.disposition == disposition)

    query = query.order_by(Call.created_at.desc()).offset(offset).limit(limit)
    results = db.session.execute(query).scalars().all()

    count_query = select(func.count()).select_from(Call).where(
        Call.company_id == company_id
    )
    if lead_id:
        count_query = count_query.where(Call.lead_id == lead_id)

    total = db.session.execute(count_query).scalar_one()

    return jsonify({
        'calls': [c.to_dict() for c in results],
        'total': total,
        'limit': limit,
        'offset': offset,
    })


# ---------------------------------------------------------------------------
# Outbound call — POST /outbound
# ---------------------------------------------------------------------------

@sms_call_bp.route('/outbound', methods=['POST'])
@require_auth_json()
@require_csrf
def outbound():
    """Trigger an outbound call via the SMS connector.

    Request body (TriggerCallPayload): { to_phone, lead_id?, twiml_url? }
    Response matches TriggerCallResponse:
        { success, call_id, status, provider_sid, to_phone }
    """
    data = request.get_json()
    if not data:
        return jsonify({'error': 'JSON body required'}), 400

    company_id = _current_company_id()
    to_phone = data.get('to_phone', '').strip()
    lead_id = data.get('lead_id')
    twiml_url = data.get('twiml_url')

    if not to_phone:
        return jsonify({'error': 'to_phone is required'}), 400

    try:
        connector = _get_sms_connector(company_id)
        result = connector.make_call(
            company_id=company_id,
            to_phone=to_phone,
            lead_id=lead_id,
            twiml_url=twiml_url,
        )
        return jsonify({
            'success': True,
            'call_id': result['call_id'],
            'status': result['status'],
            'provider_sid': result['provider_sid'],
            'to_phone': result['to_phone'],
        })
    except ConnectorError as e:
        logger.error(f'Call trigger connector error: {e}')
        return jsonify({'error': 'Call provider error'}), 400
    except Exception as e:
        logger.error(f'Call trigger failed: {e}')
        return jsonify({'error': 'Failed to trigger call'}), 500


# ---------------------------------------------------------------------------
# Call disposition — PUT /call/<id>/disposition
# ---------------------------------------------------------------------------

@sms_call_bp.route('/call/<call_id>/disposition', methods=['PUT'])
@require_auth_json()
@require_csrf
def disposition(call_id):
    """Update call disposition after agent review.

    Request body: { disposition: string }
    Response: { success: boolean }
    """
    company_id = _current_company_id()

    call = db.session.execute(
        select(Call).where(
            and_(Call.id == call_id, Call.company_id == company_id)
        )
    ).scalar_one_or_none()

    if not call:
        return jsonify({'error': 'Call not found'}), 404

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

    disposition = data.get('disposition')
    valid_dispositions = (
        'connected', 'voicemail', 'no_answer',
        'busy', 'appointment_set', 'unknown',
    )
    if disposition and disposition not in valid_dispositions:
        return jsonify({
            'error': f'Invalid disposition. Must be one of: {", ".join(valid_dispositions)}'
        }), 400

    old_disposition = call.disposition
    call.disposition = disposition or call.disposition
    db.session.commit()

    # Update lead status if appointment_set
    if disposition == 'appointment_set' and call.lead_id:
        lead = db.session.get(AngiLead, call.lead_id)
        if lead:
            lead.status = 'appointment_set'
            lead.updated_at = datetime.now(timezone.utc)
            db.session.commit()

    # Create touchpoint for disposition change
    if disposition and disposition != old_disposition:
        touchpoint_map = {
            'connected': 'call_connected',
            'voicemail': 'call_voicemail',
            'no_answer': 'call_no_answer',
            'appointment_set': 'call_connected',
            'busy': 'call_no_answer',
            'unknown': 'call_outbound',
        }
        tp_type = touchpoint_map.get(disposition, 'call_outbound')
        tp = LeadTouchpoint(
            company_id=company_id,
            lead_id=call.lead_id,
            touchpoint_type=tp_type,
            direction='system',
            content=f'Call disposition: {disposition}',
            reference_id=call.id,
            reference_type='call',
        )
        db.session.add(tp)
        db.session.commit()

    return jsonify({'success': True})


# ---------------------------------------------------------------------------
# Metrics — GET /metrics?lead_id=XXX
# ---------------------------------------------------------------------------

@sms_call_bp.route('/metrics', methods=['GET'])
@require_auth_json()
def metrics():
    """Get speed-to-lead metrics for a specific lead.

    Response matches MetricsResponse:
        { lead_id, lead_created_at, time_to_first_contact_minutes,
          time_to_first_sms_minutes, time_to_first_call_minutes,
          first_contact_type, first_contact_at }
    """
    lead_id = request.args.get('lead_id')
    if not lead_id:
        return jsonify({'error': 'lead_id query parameter is required'}), 400

    company_id = _current_company_id()

    lead = db.session.get(AngiLead, lead_id)
    if not lead or lead.company_id != company_id:
        return jsonify({'error': 'Lead not found'}), 404

    # First outbound touchpoint (SMS or call)
    first_contact = db.session.execute(
        select(LeadTouchpoint).where(
            and_(
                LeadTouchpoint.lead_id == lead_id,
                LeadTouchpoint.direction == 'outbound',
                LeadTouchpoint.touchpoint_type.in_(['sms_sent', 'call_outbound']),
            )
        ).order_by(LeadTouchpoint.created_at).limit(1)
    ).scalar_one_or_none()

    # First outbound SMS
    first_sms = db.session.execute(
        select(SmsMessage).where(
            and_(
                SmsMessage.lead_id == lead_id,
                SmsMessage.direction == 'outbound',
            )
        ).order_by(SmsMessage.created_at).limit(1)
    ).scalar_one_or_none()

    # First outbound call
    first_call = db.session.execute(
        select(Call).where(
            and_(
                Call.lead_id == lead_id,
                Call.direction == 'outbound',
            )
        ).order_by(Call.created_at).limit(1)
    ).scalar_one_or_none()

    time_to_first_contact = None
    time_to_first_sms = None
    time_to_first_call = None

    if first_contact and lead.created_at:
        time_to_first_contact = (
            (first_contact.created_at - lead.created_at).total_seconds() / 60
        )

    if first_sms and lead.created_at:
        time_to_first_sms = (
            (first_sms.created_at - lead.created_at).total_seconds() / 60
        )

    if first_call and lead.created_at:
        time_to_first_call = (
            (first_call.created_at - lead.created_at).total_seconds() / 60
        )

    return jsonify({
        'lead_id': lead_id,
        'lead_created_at': lead.created_at.isoformat() if lead.created_at else None,
        'time_to_first_contact_minutes': round(time_to_first_contact, 1) if time_to_first_contact is not None else None,
        'time_to_first_sms_minutes': round(time_to_first_sms, 1) if time_to_first_sms is not None else None,
        'time_to_first_call_minutes': round(time_to_first_call, 1) if time_to_first_call is not None else None,
        'first_contact_type': first_contact.touchpoint_type if first_contact else None,
        'first_contact_at': first_contact.created_at.isoformat() if first_contact and first_contact.created_at else None,
    })