"""Outreach task creation, scheduling, approval flow and follow-up control.

Every task is a *suggestion*. LeadPilot never contacts anyone: the user opens the
profile, performs the action on LinkedIn themselves and then marks the task done.
"""

from __future__ import annotations

from datetime import date, timedelta
from typing import Any

from sqlalchemy import select
from sqlalchemy.orm import Session

from app.config import settings
from app.models import Campaign, Lead, OutreachTask, Reply, utcnow
from app.models.enums import (
    OPEN_TASK_STATUSES,
    POSITIVE_REPLIES,
    ActivityType,
    ConnectionStatus,
    MessageStatus,
    ReplyStatus,
    TaskStatus,
    TaskType,
)
from app.rules.scoring import is_qualified
from app.services.activity_service import log_activity
from app.services.message_service import generate_message
from app.services.scoring_service import evaluate_lead

#: Day offset of the first direct message, measured from the connection task.
FIRST_MESSAGE_OFFSET_DAYS = 1

FOLLOW_UP_SEQUENCE = [TaskType.FOLLOW_UP_1, TaskType.FOLLOW_UP_2, TaskType.FOLLOW_UP_3]


def campaign_followup_days(campaign: Campaign | None) -> list[int]:
    if campaign is not None and campaign.settings and campaign.settings.followup_days:
        return list(campaign.settings.followup_days)
    return list(settings.followup_days)


def _has_task(db: Session, lead: Lead, task_type: str) -> bool:
    return (
        db.scalars(
            select(OutreachTask.id).where(
                OutreachTask.lead_id == lead.id, OutreachTask.task_type == task_type
            )
        ).first()
        is not None
    )


def create_task(
    db: Session,
    lead: Lead,
    campaign: Campaign | None,
    task_type: str,
    scheduled_date: date,
    reason: str | None = None,
    sequence_index: int = 0,
    generate: bool = True,
) -> OutreachTask:
    """Create one outreach task, generating its suggested message deterministically."""
    task = OutreachTask(
        lead_id=lead.id,
        campaign_id=campaign.id if campaign else lead.campaign_id,
        task_type=str(task_type),
        status=TaskStatus.PENDING,
        scheduled_date=scheduled_date,
        reason=reason,
        sequence_index=sequence_index,
    )

    if generate:
        generated = generate_message(db, lead, campaign, task_type=task_type)
        task.suggested_message = generated.message
        task.template_id = generated.template_id
        if generated.warnings:
            task.notes = " ".join(generated.warnings)

    db.add(task)
    db.flush()
    log_activity(
        db,
        ActivityType.TASK_CREATED,
        lead=lead,
        task=task,
        campaign=campaign,
        description=f"Created task '{task_type}' for {scheduled_date.isoformat()}",
        payload={"taskType": str(task_type)},
    )
    return task


def create_sequence_for_lead(
    db: Session,
    lead: Lead,
    campaign: Campaign | None = None,
    start_date: date | None = None,
    include_followups: bool = True,
) -> list[OutreachTask]:
    """Build the outreach sequence for one lead.

    Rejected leads get no tasks. Leads below the campaign threshold get a
    'Review Connection' task so the user decides, rather than being dropped.
    """
    campaign = campaign or lead.campaign
    if lead.is_rejected:
        return []

    evaluation = evaluate_lead(db, lead, campaign)
    min_score = campaign.min_score if campaign else 70
    # Route on the evaluation we just ran, not on the persisted score: the two
    # disagree whenever rules changed since the lead was last scored, and it
    # would be incoherent to print fresh reasons beside a stale verdict.
    qualified = is_qualified(evaluation.score, evaluation.rejected, min_score)

    start = start_date or date.today()
    created: list[OutreachTask] = []

    first_type = TaskType.CONNECTION_REQUEST if qualified else TaskType.REVIEW_CONNECTION
    reason = _build_reason(lead, evaluation, qualified, min_score)

    if not _has_task(db, lead, first_type):
        created.append(
            create_task(db, lead, campaign, first_type, start, reason=reason, sequence_index=0)
        )

    if not qualified or not include_followups:
        db.flush()
        return created

    if not _has_task(db, lead, TaskType.FIRST_MESSAGE):
        created.append(
            create_task(
                db,
                lead,
                campaign,
                TaskType.FIRST_MESSAGE,
                start + timedelta(days=FIRST_MESSAGE_OFFSET_DAYS),
                reason="Send the opening message once the connection is accepted.",
                sequence_index=1,
            )
        )

    for index, (task_type, offset) in enumerate(
        zip(FOLLOW_UP_SEQUENCE, campaign_followup_days(campaign)), start=2
    ):
        if _has_task(db, lead, task_type):
            continue
        created.append(
            create_task(
                db,
                lead,
                campaign,
                task_type,
                start + timedelta(days=offset),
                reason=f"Scheduled follow-up on day {offset} if there is no reply.",
                sequence_index=index,
            )
        )

    db.flush()
    return created


def _build_reason(lead: Lead, evaluation: Any, qualified: bool, min_score: int) -> str:
    top = [
        f"{reason['points']:+d} {reason['name']}"
        for reason in (evaluation.reasons or [])
        if reason.get("points")
    ][:4]
    detail = ", ".join(top) if top else "no scoring rules matched"
    score = evaluation.score
    if qualified:
        return f"Score {score}/100 (>= {min_score}): {detail}."
    return f"Score {score}/100 is below the {min_score} threshold: {detail}. Review manually."


def generate_tasks_for_campaign(
    db: Session, campaign: Campaign, only_qualified: bool = True
) -> dict[str, int]:
    """Create missing tasks for every eligible lead in a campaign."""
    leads = db.scalars(
        select(Lead).where(Lead.campaign_id == campaign.id, Lead.is_rejected.is_(False))
    ).all()
    summary = {"leads": 0, "tasks": 0, "skipped": 0}
    for lead in leads:
        if only_qualified and not is_qualified(lead.score, lead.is_rejected, campaign.min_score):
            summary["skipped"] += 1
            continue
        tasks = create_sequence_for_lead(db, lead, campaign)
        if tasks:
            summary["leads"] += 1
            summary["tasks"] += len(tasks)
    db.commit()
    return summary


# ---------------------------------------------------------------------------
# Task state transitions
# ---------------------------------------------------------------------------


def approve_task(db: Session, task: OutreachTask) -> OutreachTask:
    task.status = TaskStatus.APPROVED
    task.approved_at = utcnow()
    log_activity(
        db,
        ActivityType.TASK_APPROVED,
        lead=task.lead,
        task=task,
        description=f"Approved '{task.task_type}'",
    )
    db.flush()
    return task


def complete_task(db: Session, task: OutreachTask, notes: str | None = None) -> OutreachTask:
    """Mark a task done after the user performed the action on LinkedIn themselves."""
    task.status = TaskStatus.COMPLETED
    task.completed_at = utcnow()
    if notes:
        task.notes = notes

    lead = task.lead
    if task.task_type in (TaskType.CONNECTION_REQUEST, TaskType.REVIEW_CONNECTION):
        if lead.connection_status == ConnectionStatus.NOT_CONNECTED:
            lead.connection_status = ConnectionStatus.PENDING
        # Anchor on the local calendar day, the same clock every other scheduling
        # decision uses. completed_at is stored in UTC, and on a UTC server with a
        # non-UTC operator the two disagree for part of each day, which would put
        # the whole follow-up sequence a day out.
        _reanchor_followups(db, lead, date.today())
    else:
        lead.message_status = MessageStatus.SENT

    log_activity(
        db,
        ActivityType.TASK_COMPLETED,
        lead=lead,
        task=task,
        description=f"Completed '{task.task_type}'",
    )
    db.flush()
    return task


def skip_task(db: Session, task: OutreachTask, notes: str | None = None) -> OutreachTask:
    task.status = TaskStatus.SKIPPED
    if notes:
        task.notes = notes
    log_activity(
        db,
        ActivityType.TASK_SKIPPED,
        lead=task.lead,
        task=task,
        description=f"Skipped '{task.task_type}'",
    )
    db.flush()
    return task


def reject_task(db: Session, task: OutreachTask, notes: str | None = None) -> OutreachTask:
    task.status = TaskStatus.REJECTED
    if notes:
        task.notes = notes
    log_activity(
        db,
        ActivityType.TASK_REJECTED,
        lead=task.lead,
        task=task,
        description=f"Rejected '{task.task_type}'",
    )
    db.flush()
    return task


def fail_task(db: Session, task: OutreachTask, notes: str | None = None) -> OutreachTask:
    task.status = TaskStatus.FAILED
    if notes:
        task.notes = notes
    db.flush()
    return task


def _reanchor_followups(db: Session, lead: Lead, anchor: date) -> None:
    """Re-date the remaining steps relative to when the connection actually went out."""
    campaign = lead.campaign
    offsets = {
        TaskType.FIRST_MESSAGE: FIRST_MESSAGE_OFFSET_DAYS,
    }
    for task_type, offset in zip(FOLLOW_UP_SEQUENCE, campaign_followup_days(campaign)):
        offsets[task_type] = offset

    open_tasks = db.scalars(
        select(OutreachTask).where(
            OutreachTask.lead_id == lead.id,
            OutreachTask.status.in_([s.value for s in OPEN_TASK_STATUSES]),
        )
    ).all()
    for task in open_tasks:
        offset = offsets.get(task.task_type)
        if offset is not None:
            task.scheduled_date = anchor + timedelta(days=offset)


def stop_followups(
    db: Session, lead: Lead, reason: str = "Reply received", include_connection: bool = False
) -> int:
    """Cancel every still-open follow-up for a lead. Returns how many were stopped.

    ``include_connection`` also cancels a not-yet-sent connection request, which is
    what you want when the prospect is out (not interested), but not when they
    simply replied - there the invitation may still be worth sending.
    """
    task_types = [TaskType.FIRST_MESSAGE.value] + [t.value for t in FOLLOW_UP_SEQUENCE]
    if include_connection:
        task_types += [TaskType.CONNECTION_REQUEST.value, TaskType.REVIEW_CONNECTION.value]

    open_tasks = db.scalars(
        select(OutreachTask).where(
            OutreachTask.lead_id == lead.id,
            OutreachTask.status.in_([s.value for s in OPEN_TASK_STATUSES]),
            OutreachTask.task_type.in_(task_types),
        )
    ).all()
    for task in open_tasks:
        task.status = TaskStatus.STOPPED
        task.notes = (task.notes + " " if task.notes else "") + f"Stopped: {reason}"
        log_activity(
            db,
            ActivityType.TASK_STOPPED,
            lead=lead,
            task=task,
            description=f"Stopped '{task.task_type}': {reason}",
        )
    db.flush()
    return len(open_tasks)


# ---------------------------------------------------------------------------
# Replies and connection status
# ---------------------------------------------------------------------------


def record_reply(
    db: Session,
    lead: Lead,
    reply_type: str = ReplyStatus.REPLIED,
    content: str | None = None,
    task_id: int | None = None,
) -> Reply:
    """Log a reply the user received and stop the rest of the sequence."""
    reply = Reply(
        lead_id=lead.id,
        task_id=task_id,
        campaign_id=lead.campaign_id,
        reply_type=str(reply_type),
        content=content,
    )
    db.add(reply)

    lead.reply_status = str(reply_type)
    lead.message_status = MessageStatus.REPLIED
    if reply_type in POSITIVE_REPLIES:
        lead.connection_status = ConnectionStatus.CONNECTED
    elif reply_type == ReplyStatus.NOT_INTERESTED:
        lead.connection_status = ConnectionStatus.NOT_INTERESTED

    stop = True
    if lead.campaign and lead.campaign.settings:
        stop = lead.campaign.settings.stop_followups_on_reply
    if stop and reply_type != ReplyStatus.NO_REPLY:
        stop_followups(
            db,
            lead,
            reason=f"Reply recorded ({reply_type})",
            include_connection=reply_type == ReplyStatus.NOT_INTERESTED,
        )

    log_activity(
        db,
        ActivityType.REPLY_RECEIVED,
        lead=lead,
        description=f"Reply recorded: {reply_type}",
        payload={"replyType": str(reply_type)},
    )
    db.flush()
    return reply


def update_connection_status(db: Session, lead: Lead, status: str) -> Lead:
    previous = lead.connection_status
    lead.connection_status = str(status)
    if status == ConnectionStatus.NOT_INTERESTED:
        # The prospect is out, so a pending invitation should not stay in the queue.
        stop_followups(db, lead, reason="Marked not interested", include_connection=True)
    log_activity(
        db,
        ActivityType.CONNECTION_STATUS_CHANGED,
        lead=lead,
        description=f"Connection status {previous} -> {status}",
        payload={"from": previous, "to": str(status)},
    )
    db.flush()
    return lead
