"""과거 office/hwp pending 문서 markdown stage 백필 — plan ds-s1-backend-1 C-4.

신규 ingest 는 classify→markdown 전이(queue_consumer.py:142)로 자동 도달하므로 이 스크립트는
*과거* office/hwp 행만 다룬다. C-2 가 office_md 변환을 붙이기 전까지 markdown stage 에서
skip 된 행들을 다시 큐에 넣어 md_content 를 생성한다.

대상 (WHERE):
  - file_format IN (office_md 지원 실값: docx, xlsx, pptx, hwp, hwpx)
    ★ 제외 축 = file_format. INCLUDE 필터가 article(file_format='article')을 구조적으로 배제
      → P0-3 가드(md 없는 article 이 completed 도달 금지, correctness-critical). source_channel 절 불필요.
    ★ 레거시 바이너리(.doc/.xls/.ppt)는 markitdown 미지원 → 기본 목록 제외(넣어도 marker 가 skip).
  - md_status = 'pending'  (이미 success/failed/skipped 는 건드리지 않음)
  - extracted_text IS NOT NULL  (폴백 존재 모집단)
      C-5 failed-postcondition 상속: 변환 실패는 md_status='failed' 로 시끄럽게 남는다(앱이
      '변환 실패' 표시). extracted_text NULL office(폴백 없음)는 배제 — 실패 시 더 시끄러운
      별 집합이라 phase2 재검토(C-4 배제 honest).

스케줄:
  ★ C-2 라이브 office ingestion 과 백필 창 비중첩 — markdown 워커는 BATCH=1 직렬이라
    야간 단발로 돌려 라이브 office 업로드 stall 회피(plan C-2 reflection).

실행:
  docker compose exec fastapi python /app/scripts/backfill_nonpdf_markdown.py --dry-run
  docker compose exec fastapi python /app/scripts/backfill_nonpdf_markdown.py --apply
  docker compose exec fastapi python /app/scripts/backfill_nonpdf_markdown.py --apply --limit 50
"""

import argparse
import asyncio
import json
import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "app"))

from sqlalchemy import bindparam, text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine

# office_md 가 실제 변환하는 file_format(확장자 소문자, 점 없음). 단일 source.
DEFAULT_FORMATS = ("docx", "xlsx", "pptx", "hwp", "hwpx")

CANDIDATES_SQL = text(
    """
    SELECT id, file_format, title, file_path
    FROM documents
    WHERE deleted_at IS NULL
      AND md_status = 'pending'
      AND extracted_text IS NOT NULL
      AND file_format IN :formats
    ORDER BY id
    """
).bindparams(bindparam("formats", expanding=True))

# 활성 markdown 큐 행이 없는 doc 만 통과 (UNIQUE 부분 인덱스). 충돌 = silent skip.
ENQUEUE_SQL = text(
    """
    INSERT INTO processing_queue (document_id, stage, status, payload)
    VALUES (:doc_id, 'markdown', 'pending', CAST(:payload AS jsonb))
    ON CONFLICT DO NOTHING
    """
)


def _chunks(seq, size):
    for i in range(0, len(seq), size):
        yield seq[i : i + size]


async def run(*, apply: bool, formats: tuple[str, ...], limit: int | None, chunk_size: int) -> int:
    database_url = os.getenv(
        "DATABASE_URL", "postgresql+asyncpg://pkm:pkm@localhost:5432/pkm"
    )
    engine = create_async_engine(database_url)
    session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

    try:
        async with session_factory() as session:
            rows = (
                await session.execute(CANDIDATES_SQL, {"formats": list(formats)})
            ).all()
            if limit:
                rows = rows[:limit]

            print(f"=== office/hwp pending 후보 = {len(rows)}건 (formats={','.join(formats)}) ===")
            if not rows:
                print("후보 없음 — 종료.")
                return 0

            by_fmt: dict[str, int] = {}
            for r in rows:
                by_fmt[r.file_format] = by_fmt.get(r.file_format, 0) + 1
            print("포맷별:", ", ".join(f"{k}={v}" for k, v in sorted(by_fmt.items())))
            for r in rows[:20]:
                print(f"  id={r.id:>7}  {r.file_format:<5}  {(r.title or '')[:70]}")
            if len(rows) > 20:
                print(f"  ... 외 {len(rows) - 20}건")

            if not apply:
                print(f"\n[dry-run] {len(rows)}건 markdown 큐 enqueue 예정. --apply 로 실제 적용.")
                print("        (적용 전 C-2 라이브 office ingestion 과 비중첩 야간창 확인.)")
                return 0

            payload = json.dumps(
                {"force_reprocess": True, "reason": "c4_nonpdf_markdown_backfill"}
            )
            inserted = 0
            processed = 0
            for batch in _chunks(rows, chunk_size):
                for r in batch:
                    result = await session.execute(
                        ENQUEUE_SQL, {"doc_id": r.id, "payload": payload}
                    )
                    if result.rowcount > 0:
                        inserted += 1
                await session.commit()
                processed += len(batch)
                print(f"[apply] {processed}/{len(rows)} 처리 (enqueue 누적 {inserted})")

            print(f"\n[apply] 완료 — {inserted}/{len(rows)} 신규 markdown 큐 추가.")
            print("        (skip = 이미 활성 markdown 큐 행이 있는 문서)")
            return 0
    finally:
        await engine.dispose()


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--apply", action="store_true", help="실제 enqueue (기본 dry-run)")
    parser.add_argument("--dry-run", action="store_true", help="명시적 dry-run (default 동등)")
    parser.add_argument(
        "--formats", type=str, default=",".join(DEFAULT_FORMATS),
        help=f"쉼표 구분 file_format (기본 {','.join(DEFAULT_FORMATS)})",
    )
    parser.add_argument("--limit", type=int, default=None, help="후보 상한(샘플 검증용)")
    parser.add_argument("--chunk", type=int, default=200, help="enqueue txn 청크 크기")
    args = parser.parse_args()
    if args.apply and args.dry_run:
        parser.error("--apply 와 --dry-run 동시 지정 불가")
    formats = tuple(f.strip().lower() for f in args.formats.split(",") if f.strip())
    return asyncio.run(
        run(apply=args.apply, formats=formats, limit=args.limit, chunk_size=args.chunk)
    )


if __name__ == "__main__":
    sys.exit(main())
