"""Prospect CRUD, filtering, scoring, CSV import and CSV export."""

from __future__ import annotations

from typing import Any

from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Response, UploadFile, status
from sqlalchemy import func, select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session

from app.database import get_db
from app.models import Campaign, ImportHistory, Lead
from app.models.enums import LeadStatus
from app.rules.scoring import is_qualified, status_for_score
from app.schemas.base import Message, Page
from app.schemas.lead import (
    ActivityRead,
    ConnectionStatusUpdate,
    LeadCreate,
    LeadDetail,
    LeadRead,
    LeadScoreRead,
    LeadUpdate,
    ScoreRequest,
    ScoreResponse,
)
from app.schemas.outreach import ImportHistoryRead, ImportResultRead, JsonImportRequest
from app.services import lead_service, outreach_service, scoring_service
from app.utils.csv_utils import EXPORT_COLUMNS, read_csv, write_csv
from app.utils.dedupe import build_dedupe_key

router = APIRouter(prefix="/leads", tags=["leads"])


def get_lead_or_404(db: Session, lead_id: int) -> Lead:
    lead = db.get(Lead, lead_id)
    if lead is None:
        raise HTTPException(status_code=404, detail=f"Lead {lead_id} not found")
    return lead


def serialize(lead: Lead) -> LeadRead:
    payload = LeadRead.model_validate(lead)
    payload.campaign_name = lead.campaign.name if lead.campaign else None
    return payload


@router.get("", response_model=Page[LeadRead])
def list_leads(
    db: Session = Depends(get_db),
    campaign_id: int | None = Query(default=None, alias="campaignId"),
    status_filter: list[str] | None = Query(default=None, alias="status"),
    connection_status: list[str] | None = Query(default=None, alias="connectionStatus"),
    message_status: list[str] | None = Query(default=None, alias="messageStatus"),
    reply_status: list[str] | None = Query(default=None, alias="replyStatus"),
    industry: str | None = None,
    location: str | None = None,
    job_title: str | None = Query(default=None, alias="jobTitle"),
    min_score: int | None = Query(default=None, alias="minScore"),
    max_score: int | None = Query(default=None, alias="maxScore"),
    search: str | None = None,
    include_rejected: bool = Query(default=True, alias="includeRejected"),
    sort: str = "score",
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=50, ge=1, le=500, alias="pageSize"),
) -> Page[LeadRead]:
    query = lead_service.build_lead_query(
        campaign_id=campaign_id,
        status=status_filter,
        connection_status=connection_status,
        message_status=message_status,
        reply_status=reply_status,
        industry=industry,
        location=location,
        job_title=job_title,
        min_score=min_score,
        max_score=max_score,
        search=search,
        include_rejected=include_rejected,
        sort=sort,
    )
    total = int(db.scalar(select(func.count()).select_from(query.subquery())) or 0)
    rows = db.scalars(query.offset((page - 1) * page_size).limit(page_size)).all()
    return Page[LeadRead](
        items=[serialize(lead) for lead in rows], total=total, page=page, page_size=page_size
    )


@router.post("", response_model=LeadRead, status_code=status.HTTP_201_CREATED)
def create_lead(payload: LeadCreate, db: Session = Depends(get_db)) -> LeadRead:
    campaign = db.get(Campaign, payload.campaign_id) if payload.campaign_id else None
    if payload.campaign_id and campaign is None:
        raise HTTPException(status_code=404, detail=f"Campaign {payload.campaign_id} not found")

    data = payload.model_dump()
    try:
        lead, created = lead_service.create_lead(db, data, campaign, source=payload.source)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail=str(exc)) from exc

    if created and campaign is not None:
        settings_obj = campaign.settings
        if settings_obj is None or settings_obj.auto_create_tasks:
            outreach_service.create_sequence_for_lead(db, lead, campaign)
    db.commit()
    db.refresh(lead)
    return serialize(lead)


@router.get("/export")
def export_leads(
    db: Session = Depends(get_db),
    campaign_id: int | None = Query(default=None, alias="campaignId"),
    scope: str = Query(default="all", pattern="^(all|qualified|hot|rejected|campaign)$"),
) -> Response:
    """Export leads as CSV. ``scope`` selects the preset the UI offers."""
    status_filter: list[str] | None = None
    include_rejected = True
    if scope == "qualified":
        status_filter = [LeadStatus.QUALIFIED.value, LeadStatus.HOT.value]
        include_rejected = False
    elif scope == "hot":
        status_filter = [LeadStatus.HOT.value]
        include_rejected = False
    elif scope == "rejected":
        status_filter = [LeadStatus.REJECTED.value]

    query = lead_service.build_lead_query(
        campaign_id=campaign_id, status=status_filter, include_rejected=include_rejected
    )
    rows = db.scalars(query).all()
    csv_text = write_csv(list(rows), EXPORT_COLUMNS)
    filename = f"leadpilot-{scope}-leads.csv"
    return Response(
        content=csv_text,
        media_type="text/csv",
        headers={"Content-Disposition": f'attachment; filename="{filename}"'},
    )


@router.post("/import", response_model=ImportResultRead)
async def import_leads_file(
    file: UploadFile = File(...),
    campaign_id: int | None = Form(default=None),
    update_existing: bool = Form(default=False),
    dry_run: bool = Form(default=False),
    db: Session = Depends(get_db),
) -> ImportResultRead:
    """CSV upload -> validation -> duplicate detection -> rule engine -> score."""
    raw = await file.read()
    if not raw:
        raise HTTPException(status_code=422, detail="The uploaded file is empty")

    rows, recognised, unknown = read_csv(raw)
    if not recognised:
        raise HTTPException(
            status_code=422,
            detail="No recognised columns. Expected headers such as fullName, jobTitle, company.",
        )

    campaign = db.get(Campaign, campaign_id) if campaign_id else None
    if campaign_id and campaign is None:
        raise HTTPException(status_code=404, detail=f"Campaign {campaign_id} not found")

    result = lead_service.import_leads(
        db,
        rows,
        campaign,
        filename=file.filename or "upload.csv",
        update_existing=update_existing,
        dry_run=dry_run,
        unknown_headers=unknown,
    )
    if not dry_run and campaign is not None and result.lead_ids:
        _create_tasks_for_imported(db, campaign, result.lead_ids)
    return ImportResultRead.model_validate(result.as_dict())


@router.post("/import-json", response_model=ImportResultRead)
def import_leads_json(payload: JsonImportRequest, db: Session = Depends(get_db)) -> ImportResultRead:
    """Import rows posted as JSON, or a pasted CSV blob."""
    rows: list[dict[str, Any]] = []
    unknown: list[str] = []
    if payload.csv_text:
        rows, recognised, unknown = read_csv(payload.csv_text)
        if not recognised:
            raise HTTPException(status_code=422, detail="No recognised columns in the pasted CSV")
    if payload.rows:
        rows.extend(payload.rows)
    if not rows:
        raise HTTPException(status_code=422, detail="Nothing to import")

    campaign = db.get(Campaign, payload.campaign_id) if payload.campaign_id else None
    if payload.campaign_id and campaign is None:
        raise HTTPException(status_code=404, detail=f"Campaign {payload.campaign_id} not found")

    result = lead_service.import_leads(
        db,
        rows,
        campaign,
        filename=payload.filename,
        update_existing=payload.update_existing,
        dry_run=payload.dry_run,
        unknown_headers=unknown,
    )
    if not payload.dry_run and campaign is not None and result.lead_ids:
        _create_tasks_for_imported(db, campaign, result.lead_ids)
    return ImportResultRead.model_validate(result.as_dict())


def _create_tasks_for_imported(db: Session, campaign: Campaign, lead_ids: list[int]) -> None:
    settings_obj = campaign.settings
    if settings_obj is not None and not settings_obj.auto_create_tasks:
        return
    for lead_id in lead_ids:
        lead = db.get(Lead, lead_id)
        if lead is not None:
            outreach_service.create_sequence_for_lead(db, lead, campaign)
    db.commit()


@router.get("/import-history", response_model=list[ImportHistoryRead])
def import_history(db: Session = Depends(get_db), limit: int = 25) -> list[ImportHistoryRead]:
    rows = db.scalars(
        select(ImportHistory).order_by(ImportHistory.id.desc()).limit(limit)
    ).all()
    return [ImportHistoryRead.model_validate(row) for row in rows]


@router.post("/score", response_model=ScoreResponse)
def score_lead_endpoint(payload: ScoreRequest, db: Session = Depends(get_db)) -> ScoreResponse:
    """Score a saved lead, or an ad-hoc prospect sent by the extension."""
    campaign = db.get(Campaign, payload.campaign_id) if payload.campaign_id else None
    if payload.campaign_id and campaign is None:
        raise HTTPException(status_code=404, detail=f"Campaign {payload.campaign_id} not found")

    if payload.lead_id is not None:
        lead = get_lead_or_404(db, payload.lead_id)
        campaign = campaign or lead.campaign
        if payload.persist:
            result = scoring_service.score_lead(db, lead, campaign)
            db.commit()
        else:
            result = scoring_service.evaluate_lead(db, lead, campaign)
    elif payload.lead is not None:
        transient = Lead(**_transient_lead_fields(payload.lead.model_dump()))
        result = scoring_service.evaluate_lead(db, transient, campaign)
    else:
        raise HTTPException(status_code=422, detail="Provide either leadId or lead")

    min_score = campaign.min_score if campaign else 70
    return ScoreResponse(
        score=result.score,
        raw_score=result.raw_score,
        status=status_for_score(result.score, result.rejected),
        rejected=result.rejected,
        rejection_reason=result.rejection_reason,
        qualified=is_qualified(result.score, result.rejected, min_score),
        min_score=min_score,
        reasons=result.reasons,
        rule_results=result.rule_results_as_dicts(),
        tags=result.tags,
        errors=result.errors,
    )


def _transient_lead_fields(data: dict[str, Any]) -> dict[str, Any]:
    normalized = lead_service.normalize_row(data)
    normalized.setdefault("full_name", data.get("full_name") or "Unknown prospect")
    return normalized


@router.get("/lookup", response_model=LeadRead | None)
def lookup_lead(
    profile_url: str = Query(alias="profileUrl"),
    campaign_id: int | None = Query(default=None, alias="campaignId"),
    db: Session = Depends(get_db),
) -> LeadRead | None:
    """Find a saved prospect by LinkedIn profile URL. Used by the extension popup."""
    key = build_dedupe_key(profile_url, None, None)
    if not key:
        return None
    query = select(Lead).where(Lead.dedupe_key == key)
    if campaign_id is not None:
        query = query.where(Lead.campaign_id == campaign_id)
    lead = db.scalars(query).first()
    return serialize(lead) if lead is not None else None


@router.get("/{lead_id}", response_model=LeadDetail)
def get_lead(lead_id: int, db: Session = Depends(get_db)) -> LeadDetail:
    lead = get_lead_or_404(db, lead_id)
    # Built from LeadRead rather than straight from the ORM: the detail-only
    # fields (tasks, replies, ...) are shaped for the UI, not mirrors of the
    # relationships, so they are filled in explicitly below.
    detail = LeadDetail(**LeadRead.model_validate(lead).model_dump())
    detail.campaign_name = lead.campaign.name if lead.campaign else None
    detail.activities = [ActivityRead.model_validate(item) for item in lead.activities[:50]]
    detail.scores = [LeadScoreRead.model_validate(item) for item in lead.scores[:10]]
    detail.tasks = [
        {
            "id": task.id,
            "taskType": task.task_type,
            "status": task.status,
            "scheduledDate": task.scheduled_date.isoformat(),
            "message": task.final_message,
            "reason": task.reason,
            "completedAt": task.completed_at.isoformat() if task.completed_at else None,
        }
        for task in sorted(lead.tasks, key=lambda t: (t.sequence_index, t.id))
    ]
    detail.replies = [
        {
            "id": reply.id,
            "replyType": reply.reply_type,
            "content": reply.content,
            "receivedAt": reply.received_at.isoformat(),
        }
        for reply in lead.replies
    ]
    latest = lead.scores[0] if lead.scores else None
    detail.rule_results = latest.rule_results if latest else []
    return detail


@router.put("/{lead_id}", response_model=LeadRead)
def update_lead(lead_id: int, payload: LeadUpdate, db: Session = Depends(get_db)) -> LeadRead:
    lead = get_lead_or_404(db, lead_id)
    data = payload.model_dump(exclude_unset=True)

    status_fields = {
        key: data.pop(key)
        for key in ("connection_status", "message_status", "reply_status")
        if key in data
    }
    try:
        # The whole write is guarded, not just the commit: (campaign_id, profile_url)
        # is unique and the collision surfaces on the flush inside update_lead.
        lead_service.update_lead(db, lead, data)
        for key, value in status_fields.items():
            if value is not None:
                setattr(lead, key, value)
        db.commit()
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(
            status_code=409,
            detail="Another prospect in this campaign already uses that profile URL",
        ) from exc
    db.refresh(lead)
    return serialize(lead)


@router.delete("/{lead_id}", response_model=Message)
def delete_lead(lead_id: int, db: Session = Depends(get_db)) -> Message:
    lead = get_lead_or_404(db, lead_id)
    name = lead.full_name
    db.delete(lead)
    db.commit()
    return Message(detail=f"Deleted {name}")


@router.post("/{lead_id}/rescore", response_model=ScoreResponse)
def rescore_lead(lead_id: int, db: Session = Depends(get_db)) -> ScoreResponse:
    lead = get_lead_or_404(db, lead_id)
    result = scoring_service.score_lead(db, lead, lead.campaign)
    db.commit()
    min_score = lead.campaign.min_score if lead.campaign else 70
    return ScoreResponse(
        score=result.score,
        raw_score=result.raw_score,
        status=status_for_score(result.score, result.rejected),
        rejected=result.rejected,
        rejection_reason=result.rejection_reason,
        qualified=is_qualified(result.score, result.rejected, min_score),
        min_score=min_score,
        reasons=result.reasons,
        rule_results=result.rule_results_as_dicts(),
        tags=result.tags,
        errors=result.errors,
    )


@router.post("/{lead_id}/connection-status", response_model=LeadRead)
def set_connection_status(
    lead_id: int, payload: ConnectionStatusUpdate, db: Session = Depends(get_db)
) -> LeadRead:
    lead = get_lead_or_404(db, lead_id)
    outreach_service.update_connection_status(db, lead, payload.status.value)
    db.commit()
    db.refresh(lead)
    return serialize(lead)


@router.post("/{lead_id}/tasks", response_model=Message)
def create_lead_tasks(lead_id: int, db: Session = Depends(get_db)) -> Message:
    lead = get_lead_or_404(db, lead_id)
    tasks = outreach_service.create_sequence_for_lead(db, lead, lead.campaign)
    db.commit()
    return Message(detail=f"Created {len(tasks)} task(s) for {lead.full_name}")
