from __future__ import annotations

from datetime import datetime, timezone

from sqlalchemy.orm import Session

from app.models import Job, JobScore, JobSource, Profile, ScrapeRun, User
from app.scrapers import SCRAPER_CLASSES, get_scraper
from app.scrapers.base import JobDTO
from app.services.contacts import extract_contacts
from app.services.job_urls import resolve_job_url
from app.services.matching import score_job_for_profile


def _safe_url(dto: JobDTO) -> str:
    url, _ = resolve_job_url(
        dto.url, source=dto.source or "", title=dto.title or "", location=dto.location or ""
    )
    return url


def _apply_contacts(job: Job, dto: JobDTO) -> None:
    info = extract_contacts(dto.description or "", dto.raw if isinstance(dto.raw, dict) else None)
    job.contact_email = info.get("contact_email") or job.contact_email
    job.contact_phone = info.get("contact_phone") or job.contact_phone
    job.contact_name = info.get("contact_name") or job.contact_name
    job.contact_linkedin = info.get("contact_linkedin") or job.contact_linkedin
    job.contacts_json = {
        "emails": info.get("contact_emails") or [],
        "phones": info.get("contact_phones") or [],
        "links": info.get("contact_links") or [],
    }


def upsert_job(db: Session, dto: JobDTO) -> tuple[Job, bool]:
    existing = (
        db.query(Job)
        .filter(Job.source == dto.source, Job.external_id == dto.external_id)
        .first()
    )
    fp = dto.fingerprint()
    if existing:
        existing.title = dto.title
        existing.company = dto.company
        existing.location = dto.location
        existing.remote = dto.remote
        existing.salary_min = dto.salary_min
        existing.salary_max = dto.salary_max
        existing.salary_currency = dto.salary_currency
        existing.url = _safe_url(dto)
        existing.description = dto.description
        existing.lang = dto.lang
        existing.tags = dto.tags
        existing.requires_english_fluent = dto.requires_english_fluent
        existing.hire_from_spain_ok = dto.hire_from_spain_ok
        existing.posted_at = dto.posted_at
        existing.raw_json = dto.raw
        existing.fingerprint = fp
        existing.scraped_at = datetime.now(timezone.utc)
        _apply_contacts(existing, dto)
        return existing, False

    # Soft dedup by fingerprint across sources
    twin = db.query(Job).filter(Job.fingerprint == fp).first()
    if twin:
        twin.scraped_at = datetime.now(timezone.utc)
        if dto.description and len(dto.description) > len(twin.description or ""):
            twin.description = dto.description
            _apply_contacts(twin, dto)
        return twin, False

    job = Job(
        external_id=dto.external_id,
        source=dto.source,
        title=dto.title,
        company=dto.company,
        location=dto.location,
        remote=dto.remote,
        salary_min=dto.salary_min,
        salary_max=dto.salary_max,
        salary_currency=dto.salary_currency,
        url=_safe_url(dto),
        description=dto.description,
        lang=dto.lang,
        tags=dto.tags,
        requires_english_fluent=dto.requires_english_fluent,
        hire_from_spain_ok=dto.hire_from_spain_ok,
        posted_at=dto.posted_at,
        raw_json=dto.raw,
        fingerprint=fp,
    )
    _apply_contacts(job, dto)
    db.add(job)
    return job, True


def recompute_scores_for_users(db: Session, jobs: list[Job] | None = None) -> None:
    users = db.query(User).filter(User.is_active.is_(True)).all()
    job_list = jobs if jobs is not None else db.query(Job).order_by(Job.scraped_at.desc()).limit(2000).all()
    for user in users:
        profile = user.profile
        if not profile:
            continue
        for job in job_list:
            score, reasons = score_job_for_profile(job, profile)
            row = (
                db.query(JobScore)
                .filter(JobScore.user_id == user.id, JobScore.job_id == job.id)
                .first()
            )
            if row:
                row.match_score = score
                row.reasons = reasons
                row.updated_at = datetime.now(timezone.utc)
            else:
                db.add(
                    JobScore(
                        user_id=user.id,
                        job_id=job.id,
                        match_score=score,
                        reasons=reasons,
                    )
                )


def run_scraper(db: Session, source_key: str) -> ScrapeRun:
    source = db.query(JobSource).filter(JobSource.key == source_key).first()
    run = ScrapeRun(source=source_key, status="running")
    db.add(run)
    db.commit()
    db.refresh(run)

    if source_key == "manual":
        run.status = "skipped"
        run.finished_at = datetime.now(timezone.utc)
        db.commit()
        return run

    try:
        scraper = get_scraper(source_key, source.config if source else {})
        dtos = scraper.fetch()
        created = updated = 0
        touched: list[Job] = []
        for dto in dtos:
            job, is_new = upsert_job(db, dto)
            touched.append(job)
            if is_new:
                created += 1
            else:
                updated += 1
        db.flush()
        recompute_scores_for_users(db, touched)
        run.fetched = len(dtos)
        run.created = created
        run.updated = updated
        run.status = "ok"
        if source:
            source.last_run_at = datetime.now(timezone.utc)
            source.last_status = "ok"
            source.last_error = None
    except Exception as exc:  # noqa: BLE001
        run.status = "error"
        run.errors = str(exc)
        if source:
            source.last_run_at = datetime.now(timezone.utc)
            source.last_status = "error"
            source.last_error = str(exc)

    run.finished_at = datetime.now(timezone.utc)
    db.commit()
    db.refresh(run)
    return run


def run_all_enabled(db: Session, user: User | None = None) -> list[ScrapeRun]:
    from app.services.entitlements import get_entitlements, source_allowed_for_user

    sources = (
        db.query(JobSource)
        .filter(JobSource.enabled.is_(True))
        .order_by(JobSource.priority.asc())
        .all()
    )
    results: list[ScrapeRun] = []
    ents = get_entitlements(db, user) if user else None
    max_sources = int(ents.get("max_sources", 9999)) if ents else 9999
    used = 0
    for src in sources:
        if src.key == "manual":
            continue
        if user and not source_allowed_for_user(db, user, src):
            continue
        if used >= max_sources:
            break
        results.append(run_scraper(db, src.key))
        used += 1
    return results
