from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import or_
from sqlalchemy.orm import Session

from app.api.deps import get_current_user
from app.core.database import get_db
from app.models import Application, Job, JobScore, JobSource, ScrapeRun, User
from app.schemas import JobListOut, JobOut, JobSourceOut, JobSourceUpdate
from app.schemas.cv_apply import ManualJobIn, ScrapeRunOut
from app.scrapers.base import JobDTO
from app.services.job_urls import resolve_job_url
from app.services.matching import score_job_for_profile
from app.services.scrape_service import recompute_scores_for_users, run_all_enabled, run_scraper, upsert_job

router = APIRouter(tags=["jobs"])


def _job_out(job: Job, user: User, db: Session, signals: dict | None = None) -> JobOut:
    score_row = (
        db.query(JobScore)
        .filter(JobScore.user_id == user.id, JobScore.job_id == job.id)
        .first()
    )
    app = (
        db.query(Application)
        .filter(Application.user_id == user.id, Application.job_id == job.id)
        .first()
    )
    resolved, meta = resolve_job_url(
        job.url, source=job.source or "", title=job.title or "", location=job.location or ""
    )
    persist = meta.get("repaired") and resolved != (job.url or "")
    if persist:
        job.url = resolved
    data = JobOut.model_validate(job)
    data.url = resolved
    data.url_openable = bool(meta.get("openable", True))
    data.url_is_search = bool(meta.get("is_search"))
    if data.contacts_json is None:
        data.contacts_json = {}
    if data.tags is None:
        data.tags = []
    data.match_score = score_row.match_score if score_row else None
    data.match_reasons = score_row.reasons if score_row else []
    data.application_status = app.status if app else None
    data.signals = signals
    return data


@router.get("/jobs", response_model=JobListOut)
def list_jobs(
    q: str | None = None,
    source: str | None = None,
    location: str | None = None,
    remote_only: bool | None = None,
    min_salary: int | None = None,
    hide_english_fluent: bool | None = None,
    min_score: float | None = None,
    only_new: bool = False,
    hide_ghosts: bool = False,
    sort: str = Query("score", pattern="^(score|date|salary)$"),
    limit: int = Query(50, ge=1, le=200),
    offset: int = Query(0, ge=0),
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    query = db.query(Job)
    if q:
        raw = q.strip()
        # "a OR b" → cualquiera; "a, b" / "a b" → todas las palabras
        if " OR " in raw.upper():
            parts = [p.strip() for p in raw.replace(" or ", " OR ").replace(" Or ", " OR ").split(" OR ") if p.strip()]
            clauses = []
            for term in parts:
                like = f"%{term}%"
                clauses.append(
                    or_(
                        Job.title.ilike(like),
                        Job.company.ilike(like),
                        Job.description.ilike(like),
                        Job.location.ilike(like),
                    )
                )
            if clauses:
                query = query.filter(or_(*clauses))
        else:
            terms = [t.strip() for t in raw.replace(",", " ").split() if t.strip()]
            for term in terms:
                like = f"%{term}%"
                query = query.filter(
                    or_(
                        Job.title.ilike(like),
                        Job.company.ilike(like),
                        Job.description.ilike(like),
                        Job.location.ilike(like),
                    )
                )
    if source:
        query = query.filter(Job.source == source)
    if location:
        loc = f"%{location.strip()}%"
        query = query.filter(or_(Job.location.ilike(loc), Job.description.ilike(loc), Job.title.ilike(loc)))
    if remote_only:
        query = query.filter(Job.remote.is_(True))
    if min_salary is not None:
        query = query.filter(Job.salary_min.is_not(None), Job.salary_min >= min_salary)
    if hide_english_fluent:
        query = query.filter(Job.requires_english_fluent.is_(False))
    if only_new and user.last_feed_visit:
        query = query.filter(Job.scraped_at >= user.last_feed_visit)

    jobs = query.all()
    # ensure scores
    missing = [j for j in jobs if not any(s.user_id == user.id for s in j.scores)]
    if missing and user.profile:
        for j in missing:
            sc, reasons = score_job_for_profile(j, user.profile)
            db.add(JobScore(user_id=user.id, job_id=j.id, match_score=sc, reasons=reasons))
        db.commit()
        for j in jobs:
            db.refresh(j)

    score_map = {
        s.job_id: s
        for s in db.query(JobScore).filter(JobScore.user_id == user.id).all()
    }
    if min_score is not None:
        jobs = [j for j in jobs if (score_map.get(j.id) and score_map[j.id].match_score >= min_score)]

    def sort_key(j: Job):
        if sort == "salary":
            return j.salary_min or 0
        if sort == "date":
            return (j.posted_at or j.scraped_at or datetime.min.replace(tzinfo=timezone.utc)).timestamp()
        return score_map[j.id].match_score if j.id in score_map else 0

    jobs.sort(key=sort_key, reverse=True)
    from app.services.wow_tier_d import attach_signals_to_jobs, ghost_risk, twin_counts

    if hide_ghosts:
        counts = twin_counts(db, jobs)
        jobs = [j for j in jobs if ghost_risk(j, counts.get(j.id, 1)).get("level") != "alta"]
    total = len(jobs)
    page = jobs[offset : offset + limit]
    user.last_feed_visit = datetime.now(timezone.utc)
    sig_map = attach_signals_to_jobs(db, user, page, score_map)
    items = [_job_out(j, user, db, signals=sig_map.get(j.id)) for j in page]
    db.commit()
    return JobListOut(total=total, items=items)


@router.get("/jobs/{job_id}", response_model=JobOut)
def get_job(job_id: int, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    job = db.get(Job, job_id)
    if not job:
        raise HTTPException(404, "Oferta no encontrada")
    out = _job_out(job, user, db)
    from app.services.wow_tier_d import job_signals, signal_gates, twin_counts, twin_siblings

    gates = signal_gates(db, user)
    count = twin_counts(db, [job]).get(job.id, 1)
    out.signals = job_signals(
        job,
        gates=gates,
        twin_count=count,
        twins=twin_siblings(db, job) if gates.get("twins") else [],
        reasons=out.match_reasons,
    )
    db.commit()
    return out


@router.post("/jobs/{job_id}/extract-contacts", response_model=JobOut)
def extract_job_contacts(
    job_id: int,
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    from app.services.contacts import extract_contacts

    job = db.get(Job, job_id)
    if not job:
        raise HTTPException(404, "Oferta no encontrada")
    info = extract_contacts(job.description or "", job.raw_json if isinstance(job.raw_json, dict) else None)
    job.contact_email = info.get("contact_email")
    job.contact_phone = info.get("contact_phone")
    job.contact_name = info.get("contact_name")
    job.contact_linkedin = info.get("contact_linkedin")
    job.contacts_json = {
        "emails": info.get("contact_emails") or [],
        "phones": info.get("contact_phones") or [],
        "links": info.get("contact_links") or [],
    }
    db.commit()
    db.refresh(job)
    return _job_out(job, user, db)


@router.post("/jobs/manual", response_model=JobOut)
def add_manual_job(
    payload: ManualJobIn,
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    dto = JobDTO(
        external_id=payload.url,
        source=payload.source or "manual",
        title=payload.title,
        company=payload.company,
        location=payload.location,
        remote=payload.remote,
        salary_min=payload.salary_min,
        salary_max=payload.salary_max,
        salary_currency=payload.salary_currency,
        url=payload.url,
        description=payload.description,
        tags=payload.tags,
        lang="en",
        raw={"manual": True},
    )
    job, _ = upsert_job(db, dto)
    db.flush()
    if user.profile:
        sc, reasons = score_job_for_profile(job, user.profile)
        existing = (
            db.query(JobScore)
            .filter(JobScore.user_id == user.id, JobScore.job_id == job.id)
            .first()
        )
        if existing:
            existing.match_score = sc
            existing.reasons = reasons
        else:
            db.add(JobScore(user_id=user.id, job_id=job.id, match_score=sc, reasons=reasons))
    db.commit()
    db.refresh(job)
    return _job_out(job, user, db)


@router.get("/sources", response_model=list[JobSourceOut])
def list_sources(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    from app.services.entitlements import source_allowed_for_user, user_plan_rank

    rank = user_plan_rank(db, user)
    rows = db.query(JobSource).order_by(JobSource.priority.asc(), JobSource.name.asc()).all()
    out: list[JobSourceOut] = []
    for src in rows:
        allowed = source_allowed_for_user(db, user, src)
        data = JobSourceOut.model_validate(src)
        data.allowed = allowed
        if not allowed:
            data.locked_reason = (
                f"Requiere plan superior (tier {src.tier_min}+). Tu nivel: {rank}."
                if src.tier_min > rank
                else "No incluido en las regiones de tu plan."
            )
        out.append(data)
    return out


@router.patch("/sources/{key}", response_model=JobSourceOut)
def update_source(
    key: str,
    payload: JobSourceUpdate,
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    if not user.is_admin:
        raise HTTPException(403, "Solo el admin puede activar/desactivar fuentes globales")
    src = db.query(JobSource).filter(JobSource.key == key).first()
    if not src:
        raise HTTPException(404, "Fuente no encontrada")
    if payload.enabled is not None:
        src.enabled = payload.enabled
    if payload.config is not None:
        src.config = payload.config
    db.commit()
    db.refresh(src)
    return src


@router.post("/scrape/run", response_model=list[ScrapeRunOut])
def scrape_all(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    from app.services.entitlements import check_scrape_allowed, increment_usage

    check_scrape_allowed(db, user)
    results = run_all_enabled(db, user=user)
    increment_usage(db, user, "scrapes_day")
    return results


@router.post("/scrape/run/{source_key}", response_model=ScrapeRunOut)
def scrape_one(source_key: str, user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    from app.services.entitlements import check_scrape_allowed, increment_usage, source_allowed_for_user

    src = db.query(JobSource).filter(JobSource.key == source_key).first()
    if not src:
        raise HTTPException(404, "Fuente no encontrada")
    if not source_allowed_for_user(db, user, src):
        raise HTTPException(403, "Esta fuente no está incluida en tu plan. Ve a /pricing.")
    check_scrape_allowed(db, user)
    result = run_scraper(db, source_key)
    increment_usage(db, user, "scrapes_day")
    return result


@router.get("/scrape/runs", response_model=list[ScrapeRunOut])
def scrape_runs(
    limit: int = 30,
    user: User = Depends(get_current_user),
    db: Session = Depends(get_db),
):
    return db.query(ScrapeRun).order_by(ScrapeRun.started_at.desc()).limit(limit).all()


@router.post("/jobs/rescore")
def rescore(user: User = Depends(get_current_user), db: Session = Depends(get_db)):
    recompute_scores_for_users(db)
    db.commit()
    return {"ok": True}
