import os
import logging
import threading
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager
from fastapi import BackgroundTasks, FastAPI, Query, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from apscheduler.schedulers.background import BackgroundScheduler
from _shared.access_log import install as install_access_log

from .db import (
    init_db, get_announcements, get_announcement, create_announcement,
    update_announcement, delete_announcement, delete_closed_announcements, toggle_bookmark,
    update_all_statuses,
    get_profile, upsert_profile, get_matches, mark_match_read,
    get_last_collect_log, get_dashboard,
    delete_old_completed_announcements,
    get_listing_criteria, update_listing_criteria, get_listings, get_listing_matches,
    get_last_listing_collect_log, get_market_deals_for,
)
from .collector import collect_all
from .matcher import run_matching
from .notifier import notify_new_matches, notify_new_listings
from .listing_collector import collect_listings
from .listing_matcher import run_listing_matching, compute_safety, compute_valuation
from . import finance_rules
from .finance_rules_config import DISCLAIMER
from .models import (
    AnnouncementCreate, AnnouncementUpdate, ProfileUpdate,
    ListingCriteriaUpdate, SafetyCheckRequest, BudgetRequest,
)

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(levelname)s %(message)s")
logger = logging.getLogger("realestate-lab")

scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul"))


def scheduled_collect():
    """매일 09:15 — 수집 + 정리 (병렬) → 매칭 → 알림 push.

    collect_all과 delete_old_completed_announcements는 서로 다른 데이터
    영역을 건드리므로 thread 둘로 병렬화. 매칭은 두 작업 완료 후 순차
    실행 (DB 일관성). CHECK_POINT 중기-8 — env이 BackgroundScheduler+
    동기 함수 조합이라 asyncio.gather 대신 ThreadPoolExecutor 사용.
    """
    logger.info("스케줄 수집 시작")
    with ThreadPoolExecutor(max_workers=2) as ex:
        collect_future = ex.submit(collect_all)
        delete_future = ex.submit(delete_old_completed_announcements, 90)
        collect_future.result()
        deleted = delete_future.result()
    if deleted:
        logger.info("정리: %d건 삭제", deleted)
    run_matching()
    notify_new_matches()
    logger.info("스케줄 수집 + 매칭 + 알림 완료")


def scheduled_status_update():
    """매일 00:00 — 상태 갱신 + 재매칭"""
    logger.info("상태 갱신 시작")
    update_all_statuses()
    run_matching()
    logger.info("상태 갱신 + 재매칭 완료")


_listing_collect_lock = threading.Lock()


def _run_listing_pipeline():
    if not _listing_collect_lock.acquire(blocking=False):
        logger.info("매물 수집 이미 진행 중 — 건너뜀"); return
    try:
        collect_listings(); run_listing_matching(); notify_new_listings()
    finally:
        _listing_collect_lock.release()


def scheduled_listing_collect():
    logger.info("매물 스케줄 수집 시작"); _run_listing_pipeline()


@asynccontextmanager
async def lifespan(app: FastAPI):
    init_db()
    # 09:00 cron 스태거링 — agent-office 09:00/05/10 이후 (CHECK_POINT FU-A)
    scheduler.add_job(scheduled_collect, "cron", hour=9, minute=15, id="collect")
    scheduler.add_job(scheduled_status_update, "cron", hour=0, minute=0, id="status_update")
    scheduler.add_job(scheduled_listing_collect, "cron", hour="8,11,14,17,20", minute=40, id="listing_collect")
    scheduler.start()
    logger.info("realestate-lab 시작")
    yield
    scheduler.shutdown()


app = FastAPI(lifespan=lifespan)
install_access_log(app)

_cors_origins = os.getenv("CORS_ALLOW_ORIGINS", "http://localhost:3007,http://localhost:8080").split(",")
app.add_middleware(
    CORSMiddleware,
    allow_origins=[o.strip() for o in _cors_origins],
    allow_credentials=False,
    allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allow_headers=["Content-Type"],
)


@app.get("/health")
def health():
    return {"status": "ok"}


# ── 공고 API ─────────────────────────────────────────────────────────────────

@app.get("/api/realestate/announcements")
def api_announcements(
    region: str = None,
    status: str = None,
    house_type: str = None,
    matched_only: bool = False,
    bookmarked: bool = False,
    sort: str = "date",
    page: int = Query(1, ge=1),
    size: int = Query(20, ge=1, le=100),
):
    return get_announcements(region, status, house_type, matched_only, bookmarked, sort, page, size)


@app.get("/api/realestate/announcements/{ann_id}")
def api_announcement_detail(ann_id: int):
    ann = get_announcement(ann_id)
    if not ann:
        raise HTTPException(status_code=404, detail="Announcement not found")
    return ann


@app.post("/api/realestate/announcements", status_code=201)
def api_announcement_create(body: AnnouncementCreate):
    return create_announcement(body.model_dump())


@app.put("/api/realestate/announcements/{ann_id}")
def api_announcement_update(ann_id: int, body: AnnouncementUpdate):
    updated = update_announcement(ann_id, body.model_dump(exclude_none=True))
    if not updated:
        raise HTTPException(status_code=404, detail="Announcement not found")
    return updated


@app.patch("/api/realestate/announcements/{ann_id}/bookmark")
def api_announcement_bookmark(ann_id: int):
    result = toggle_bookmark(ann_id)
    if result is None:
        raise HTTPException(status_code=404, detail="Announcement not found")
    return result


@app.delete("/api/realestate/announcements/closed")
def api_announcement_delete_closed():
    """status='완료' 공고 일괄 삭제."""
    count = delete_closed_announcements()
    return {"ok": True, "deleted": count}


@app.delete("/api/realestate/announcements/{ann_id}")
def api_announcement_delete(ann_id: int):
    if not delete_announcement(ann_id):
        raise HTTPException(status_code=404, detail="Announcement not found")
    return {"ok": True}


# ── 수집 API ─────────────────────────────────────────────────────────────────

_collect_lock = threading.Lock()


def _run_collect_and_match():
    if not _collect_lock.acquire(blocking=False):
        logger.info("수집 이미 진행 중 — 건너뜀")
        return
    try:
        collect_all()
        delete_old_completed_announcements(grace_days=90)
        run_matching()
        notify_new_matches()
    finally:
        _collect_lock.release()


@app.post("/api/realestate/collect")
def api_collect(background_tasks: BackgroundTasks):
    background_tasks.add_task(_run_collect_and_match)
    return {"ok": True, "message": "수집 시작됨"}


@app.get("/api/realestate/collect/status")
def api_collect_status():
    log = get_last_collect_log()
    return log if log else {"collected_at": None, "new_count": 0, "total_count": 0, "error": None}


# ── 프로필 API ───────────────────────────────────────────────────────────────

@app.get("/api/realestate/profile")
def api_profile_get():
    profile = get_profile()
    return profile if profile else {}


@app.put("/api/realestate/profile")
def api_profile_update(body: ProfileUpdate):
    return upsert_profile(body.model_dump(exclude_none=True))


# ── 매칭 API ─────────────────────────────────────────────────────────────────

@app.get("/api/realestate/matches")
def api_matches(page: int = Query(1, ge=1), size: int = Query(20, ge=1, le=100)):
    return get_matches(page, size)


@app.post("/api/realestate/matches/refresh")
def api_matches_refresh():
    try:
        run_matching()
    except Exception as e:
        logger.exception("매칭 실행 실패")
        raise HTTPException(status_code=500, detail=str(e))
    return {"ok": True}


@app.patch("/api/realestate/matches/{match_id}/read")
def api_match_read(match_id: int):
    if not mark_match_read(match_id):
        raise HTTPException(status_code=404, detail="Match not found")
    return {"ok": True}


# ── 대시보드 API ─────────────────────────────────────────────────────────────

@app.get("/api/realestate/dashboard")
def api_dashboard():
    return get_dashboard()


# ── 매물 API ─────────────────────────────────────────────────────────────────

@app.get("/api/realestate/listings")
def api_listings(dong: str = None, deal_type: str = None, tier: str = None,
                 matched_only: bool = False, page: int = 1, size: int = 50):
    return {"listings": get_listings(dong, deal_type, tier, matched_only, size, (page - 1) * size)}


@app.post("/api/realestate/listings/collect")
def api_listings_collect(background_tasks: BackgroundTasks):
    background_tasks.add_task(_run_listing_pipeline)
    return {"ok": True, "message": "매물 수집 시작됨"}


@app.get("/api/realestate/listings/collect/status")
def api_listings_collect_status():
    return get_last_listing_collect_log() or {"status": "never_run"}


@app.get("/api/realestate/listings/criteria")
def api_get_listing_criteria():
    return get_listing_criteria()


@app.put("/api/realestate/listings/criteria")
def api_put_listing_criteria(req: ListingCriteriaUpdate):
    update_listing_criteria(req.model_dump(exclude_none=True))
    return get_listing_criteria()


@app.get("/api/realestate/listings/matches")
def api_listing_matches():
    return {"matches": get_listing_matches()}


@app.post("/api/realestate/safety-check")
def api_safety_check(req: SafetyCheckRequest):
    deals = get_market_deals_for(req.dong_code, req.complex_name, req.area,
                                 "전세" if req.deal_type != "매매" else "매매")
    reg = finance_rules.region_regulation(req.dong)   # dong명(없으면 토허 판정 생략)
    if req.deal_type == "매매":
        res = compute_valuation(req.amount, deals)
        ratio_key = "price_ratio"
    else:
        res = compute_safety(req.amount, req.deal_type, deals)
        ratio_key = "jeonse_ratio"
    return {"median": res["median"], "ratio": res.get(ratio_key), "tier": res["tier"],
            "sample": res["sample"], "is_toheo": reg["is_toheo"],
            "disclaimer": "등기부 선순위·토허 허가·실거주 의무는 계약 전 수동 확인 필수. " + DISCLAIMER}


@app.post("/api/realestate/budget")
def api_budget(req: BudgetRequest):
    reg = finance_rules.region_regulation(req.target_dong)
    return {
        "jeonse": finance_rules.estimate_jeonse_budget(req.equity, req.annual_income),
        "purchase": finance_rules.estimate_purchase_budget(req.equity, req.annual_income, reg,
                                                           req.is_first_home, req.is_homeless),
        "region": reg,
        "disclaimer": DISCLAIMER,
    }
