"""Lead creation, validation, duplicate detection and CSV import."""

from __future__ import annotations

from dataclasses import dataclass, field as dataclass_field
from typing import Any

from sqlalchemy import Select, or_, select
from sqlalchemy.orm import Session

from app.models import Campaign, ImportHistory, Lead
from app.models.enums import ActivityType
from app.rules.fields import FIELD_MAP
from app.rules.scoring import is_qualified
from app.services.activity_service import log_activity
from app.services.scoring_service import score_lead
from app.utils.csv_utils import SOURCE_ROW_KEY, parse_company_size, parse_list
from app.utils.dedupe import build_dedupe_key, normalize_profile_url

WRITABLE_FIELDS = {
    "first_name",
    "last_name",
    "full_name",
    "job_title",
    "company",
    "industry",
    "location",
    "company_size",
    "headline",
    "about",
    "profile_url",
    "website",
    "keywords",
    "tags",
    "source",
}


@dataclass(slots=True)
class RowError:
    row: int
    message: str
    data: dict[str, Any] = dataclass_field(default_factory=dict)

    def as_dict(self) -> dict[str, Any]:
        return {"row": self.row, "message": self.message, "data": self.data}


@dataclass(slots=True)
class ImportResult:
    total_rows: int = 0
    imported: int = 0
    updated: int = 0
    duplicates: int = 0
    failed: int = 0
    errors: list[RowError] = dataclass_field(default_factory=list)
    preview: list[dict[str, Any]] = dataclass_field(default_factory=list)
    lead_ids: list[int] = dataclass_field(default_factory=list)
    unknown_headers: list[str] = dataclass_field(default_factory=list)
    history_id: int | None = None

    def as_dict(self) -> dict[str, Any]:
        return {
            "totalRows": self.total_rows,
            "imported": self.imported,
            "updated": self.updated,
            "duplicates": self.duplicates,
            "failed": self.failed,
            "errors": [error.as_dict() for error in self.errors],
            "preview": self.preview,
            "leadIds": self.lead_ids,
            "unknownHeaders": self.unknown_headers,
            "historyId": self.history_id,
        }


def normalize_row(row: dict[str, Any]) -> dict[str, Any]:
    """Coerce one incoming row into the ORM field shape.

    Accepts both snake_case (``job_title``) and the camelCase the API and the
    extension speak (``jobTitle``).
    """
    data: dict[str, Any] = {}
    for key, value in row.items():
        attribute = FIELD_MAP.get(key, key)
        if attribute in WRITABLE_FIELDS:
            data[attribute] = value

    for key in ("first_name", "last_name", "full_name", "job_title", "company", "industry",
                "location", "headline", "about", "website"):
        if isinstance(data.get(key), str):
            data[key] = data[key].strip() or None

    if data.get("company_size") is not None:
        data["company_size"] = parse_company_size(data["company_size"])

    for key in ("keywords", "tags"):
        if key in data:
            data[key] = parse_list(data[key])

    if data.get("profile_url"):
        url = str(data["profile_url"]).strip()
        data["profile_url"] = url if "://" in url else f"https://{url.lstrip('/')}"

    if not data.get("full_name"):
        parts = [data.get("first_name"), data.get("last_name")]
        joined = " ".join(part for part in parts if part)
        if joined:
            data["full_name"] = joined

    if data.get("full_name") and not data.get("first_name"):
        pieces = str(data["full_name"]).split()
        data["first_name"] = pieces[0]
        if len(pieces) > 1 and not data.get("last_name"):
            data["last_name"] = " ".join(pieces[1:])

    return data


def validate_row(data: dict[str, Any]) -> str | None:
    """Return an error message when the row cannot become a lead."""
    if not data.get("full_name") and not data.get("profile_url"):
        return "row needs at least a name or a profile URL"
    if not data.get("full_name"):
        url = normalize_profile_url(data.get("profile_url")) or ""
        slug = url.rsplit("/", 1)[-1] if url else ""
        if not slug:
            return "row needs at least a name or a profile URL"
        data["full_name"] = slug.replace("-", " ").title()
    size = data.get("company_size")
    if size is not None and (size < 0 or size > 10_000_000):
        return f"company size {size} is out of range"
    return None


def find_duplicate(db: Session, dedupe_key: str | None, campaign_id: int | None) -> Lead | None:
    """A prospect is a duplicate when the same dedupe key exists in the same campaign."""
    if not dedupe_key:
        return None
    query = select(Lead).where(Lead.dedupe_key == dedupe_key)
    query = query.where(
        Lead.campaign_id == campaign_id if campaign_id is not None else Lead.campaign_id.is_(None)
    )
    return db.scalars(query).first()


def create_lead(
    db: Session,
    data: dict[str, Any],
    campaign: Campaign | None = None,
    source: str = "manual",
    run_scoring: bool = True,
) -> tuple[Lead, bool]:
    """Create (or return the existing duplicate of) a lead. Returns ``(lead, created)``."""
    payload = normalize_row(data)
    error = validate_row(payload)
    if error:
        raise ValueError(error)

    campaign_id = campaign.id if campaign else data.get("campaign_id")
    dedupe_key = build_dedupe_key(
        payload.get("profile_url"), payload.get("full_name"), payload.get("company")
    )
    existing = find_duplicate(db, dedupe_key, campaign_id)
    if existing is not None:
        return existing, False

    lead = Lead(
        campaign_id=campaign_id,
        dedupe_key=dedupe_key,
        source=payload.pop("source", source),
        **payload,
    )
    db.add(lead)
    db.flush()

    log_activity(
        db,
        ActivityType.LEAD_CREATED,
        lead=lead,
        campaign=campaign,
        description=f"Lead created from {lead.source}",
    )
    if run_scoring:
        score_lead(db, lead, campaign)
    return lead, True


def update_lead(db: Session, lead: Lead, data: dict[str, Any], run_scoring: bool = True) -> Lead:
    payload = normalize_row(data)
    for key, value in payload.items():
        setattr(lead, key, value)

    if "campaign_id" in data:
        lead.campaign_id = data["campaign_id"]

    lead.dedupe_key = build_dedupe_key(lead.profile_url, lead.full_name, lead.company)
    db.flush()

    log_activity(db, ActivityType.LEAD_UPDATED, lead=lead, description="Lead updated")
    if run_scoring:
        score_lead(db, lead, lead.campaign)
    return lead


def import_leads(
    db: Session,
    rows: list[dict[str, Any]],
    campaign: Campaign | None = None,
    filename: str = "upload.csv",
    update_existing: bool = False,
    dry_run: bool = False,
    unknown_headers: list[str] | None = None,
) -> ImportResult:
    """CSV -> validation -> duplicate detection -> rule engine -> score -> campaign."""
    result = ImportResult(total_rows=len(rows), unknown_headers=unknown_headers or [])
    campaign_id = campaign.id if campaign else None
    seen_keys: set[str] = set()

    for position, raw_row in enumerate(rows, start=2):  # line 1 is the header
        # Prefer the real file line recorded by the CSV reader; JSON rows have none.
        index = int(raw_row.get(SOURCE_ROW_KEY) or position)
        payload = normalize_row(raw_row)
        error = validate_row(payload)
        if error:
            result.failed += 1
            result.errors.append(RowError(index, error, _without_internals(raw_row)))
            continue

        dedupe_key = build_dedupe_key(
            payload.get("profile_url"), payload.get("full_name"), payload.get("company")
        )
        duplicate_in_file = bool(dedupe_key) and dedupe_key in seen_keys
        existing = None if duplicate_in_file else find_duplicate(db, dedupe_key, campaign_id)

        if duplicate_in_file or existing is not None:
            result.duplicates += 1
            if dedupe_key:
                # Remember it either way, so a row repeated three times in one file
                # is not applied (and counted) three times.
                seen_keys.add(dedupe_key)
            if dry_run:
                result.preview.append(
                    _preview_row(payload, "duplicate", "already in this campaign")
                )
                continue
            if existing is not None and update_existing:
                update_lead(db, existing, payload)
                result.updated += 1
                result.lead_ids.append(existing.id)
            continue

        if dedupe_key:
            seen_keys.add(dedupe_key)

        if dry_run:
            result.preview.append(_preview_row(payload, "new", "will be imported"))
            result.imported += 1
            continue

        try:
            lead, created = create_lead(db, payload, campaign, source="csv")
        except ValueError as exc:
            result.failed += 1
            result.errors.append(RowError(index, str(exc), _without_internals(raw_row)))
            continue

        if created:
            result.imported += 1
            result.lead_ids.append(lead.id)
            result.preview.append(
                _preview_row(
                    payload,
                    lead.status,
                    f"score {lead.score}",
                    score=lead.score,
                    lead_id=lead.id,
                )
            )
        else:
            result.duplicates += 1

    if dry_run:
        db.rollback()
        return result

    history = ImportHistory(
        filename=filename,
        campaign_id=campaign_id,
        total_rows=result.total_rows,
        imported=result.imported,
        updated=result.updated,
        duplicates=result.duplicates,
        failed=result.failed,
        errors=[error.as_dict() for error in result.errors][:200],
    )
    db.add(history)
    db.commit()
    result.history_id = history.id
    return result


def _without_internals(row: dict[str, Any]) -> dict[str, Any]:
    """Strip bookkeeping keys before echoing a row back to the user."""
    return {key: value for key, value in row.items() if key != SOURCE_ROW_KEY}


def _preview_row(
    payload: dict[str, Any],
    status: str,
    note: str,
    score: int | None = None,
    lead_id: int | None = None,
) -> dict[str, Any]:
    return {
        "leadId": lead_id,
        "fullName": payload.get("full_name"),
        "jobTitle": payload.get("job_title"),
        "company": payload.get("company"),
        "industry": payload.get("industry"),
        "location": payload.get("location"),
        "companySize": payload.get("company_size"),
        "profileUrl": payload.get("profile_url"),
        "status": status,
        "note": note,
        "score": score,
    }


# ---------------------------------------------------------------------------
# Query building for the prospects table
# ---------------------------------------------------------------------------

SORT_OPTIONS = {
    "score": (Lead.score.desc(), Lead.id.desc()),
    "score_asc": (Lead.score.asc(), Lead.id.asc()),
    "newest": (Lead.created_at.desc(), Lead.id.desc()),
    "oldest": (Lead.created_at.asc(), Lead.id.asc()),
    "name": (Lead.full_name.asc(),),
    "company": (Lead.company.asc(), Lead.full_name.asc()),
    "activity": (Lead.last_activity_at.desc(), Lead.id.desc()),
}


def build_lead_query(
    campaign_id: int | None = None,
    status: list[str] | None = None,
    connection_status: list[str] | None = None,
    message_status: list[str] | None = None,
    reply_status: list[str] | None = None,
    industry: str | None = None,
    location: str | None = None,
    job_title: str | None = None,
    min_score: int | None = None,
    max_score: int | None = None,
    search: str | None = None,
    include_rejected: bool = True,
    sort: str = "score",
) -> Select[tuple[Lead]]:
    query = select(Lead)

    if campaign_id is not None:
        query = query.where(Lead.campaign_id == campaign_id)
    if status:
        query = query.where(Lead.status.in_(status))
    if connection_status:
        query = query.where(Lead.connection_status.in_(connection_status))
    if message_status:
        query = query.where(Lead.message_status.in_(message_status))
    if reply_status:
        query = query.where(Lead.reply_status.in_(reply_status))
    if industry:
        query = query.where(Lead.industry.ilike(f"%{industry}%"))
    if location:
        query = query.where(Lead.location.ilike(f"%{location}%"))
    if job_title:
        query = query.where(Lead.job_title.ilike(f"%{job_title}%"))
    if min_score is not None:
        query = query.where(Lead.score >= min_score)
    if max_score is not None:
        query = query.where(Lead.score <= max_score)
    if not include_rejected:
        query = query.where(Lead.is_rejected.is_(False))
    if search:
        needle = f"%{search.strip()}%"
        query = query.where(
            or_(
                Lead.full_name.ilike(needle),
                Lead.company.ilike(needle),
                Lead.job_title.ilike(needle),
                Lead.headline.ilike(needle),
            )
        )

    return query.order_by(*SORT_OPTIONS.get(sort, SORT_OPTIONS["score"]))


def qualified_leads(db: Session, campaign: Campaign) -> list[Lead]:
    leads = db.scalars(select(Lead).where(Lead.campaign_id == campaign.id)).all()
    return [lead for lead in leads if is_qualified(lead.score, lead.is_rejected, campaign.min_score)]
