"""
Document Converter API — DOCX/DOC focused.
===========================================
Mục tiêu #1: output PHẢI có "Điều" (Article markers).

Pipeline DOCX:
  1. docx_converter (raw XML parse numbering.xml) → giữ "Điều %1."
  2. mammoth (DOCX → HTML → parse) → HTML renderer expand numbering
  3. Google Drive → export DOCX → docx_converter (numbering rebuilt by Google)
  4. Google Drive → export text → regex heading
  → So sánh tất cả, chọn bản nào nhiều "Điều" nhất.

Pipeline DOC (Word 97-2003 — python-docx KHÔNG đọc được):
  1. Google Drive → export DOCX → docx_converter
  2. Google Drive → export text → regex heading
  → Chọn bản nhiều "Điều" nhất.
"""

import logging
import os
import re
import tempfile
from io import BytesIO

import mammoth
from bs4 import BeautifulSoup

from fastapi import FastAPI, File, UploadFile
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

from docx_converter import convert_docx_to_markdown
from ocr_drive import get_drive_service, ocr_file

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("doc-converter")

app = FastAPI(
    title="Document Converter API",
    version="2.0.0",
    description="DOCX/DOC converter: XML parse + Google Drive OCR, "
                "bằng mọi giá phải ra 'Điều'",
)

# ── Static UI ──
_static_dir = os.path.join(os.path.dirname(__file__), "static")
if os.path.isdir(_static_dir):
    app.mount("/static", StaticFiles(directory=_static_dir, html=True), name="static")


@app.get("/")
async def root():
    index_path = os.path.join(_static_dir, "index.html")
    if os.path.exists(index_path):
        return FileResponse(index_path)
    return {"error": "index.html not found"}


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


# ─────────────────────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────────────────────

MIME_MAP = {
    ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    ".doc": "application/msword",
    ".pdf": "application/pdf",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
}

# Reverse lookup: content_type → ext (dùng khi filename không có extension)
MIME_TO_EXT = {v: k for k, v in MIME_MAP.items()}


def _count_dieu(text: str) -> int:
    """Đếm số lần xuất hiện pattern 'Điều X' / 'ĐIỀU X'."""
    return len(re.findall(r"(?:ĐIỀU|Điều)\s+\d+", text, re.UNICODE))


def _meaningful_chars(text: str) -> int:
    """Ký tự có nghĩa (bỏ whitespace, markdown markers)."""
    return len(re.sub(r"[#|*\-\s\n\r]", "", text))


def _looks_like_low_information(md_text: str) -> bool:
    """Markdown gần như rỗng."""
    if not md_text:
        return True
    lines = [l.strip() for l in md_text.splitlines() if l.strip()]
    if not lines:
        return True
    meaningful = [l for l in lines if re.sub(r"[-=\s]", "", l)]
    if not meaningful:
        return True
    if not any(re.search(r"\w", l, flags=re.UNICODE) for l in meaningful):
        return True
    separators = [l for l in lines if re.fullmatch(r"[-=]{3,}", l)]
    return len(separators) >= 3 and len(meaningful) <= 1


def _pick_best(*candidates: dict) -> dict:
    """
    Chọn candidate tốt nhất theo ưu tiên:
      1. Nhiều "Điều" nhất
      2. Nhiều chars nhất (nếu "Điều" bằng nhau)
    Mỗi candidate = {"md": str, "backend": str}
    """
    valid = [c for c in candidates if c["md"]]
    if not valid:
        return {"md": "", "backend": "none"}

    return max(valid, key=lambda c: (_count_dieu(c["md"]), len(c["md"])))


def _inject_dieu(md: str) -> str:
    """
    Last-resort: nếu output không có "Điều" nào,
    detect pattern heading dạng số + text viết HOA → inject "Điều N."

    Ví dụ:
      # 1. ĐỐI TƯỢNG CỦA HỢP ĐỒNG  →  # Điều 1. ĐỐI TƯỢNG CỦA HỢP ĐỒNG
      2. GIAO NHẬN HÀNG HÓA          →  Điều 2. GIAO NHẬN HÀNG HÓA

    Chỉ inject cho heading cấp 1 (N.), KHÔNG inject cho N.N hay N.N.N
    """
    if _count_dieu(md) > 0:
        return md  # Đã có Điều rồi, không cần inject

    # Pattern: dòng bắt đầu bằng (optional #) + số + dấu chấm + text
    # Text phải chứa >= 3 ký tự chữ HOA (để tránh inject nhầm vào dòng thường)
    _RE_NUMBERED_HEADING = re.compile(
        r"^(#{1,3}\s+)?(\d{1,2})\.\s+(.+)$",
        re.MULTILINE | re.UNICODE,
    )

    def _should_inject(text: str) -> bool:
        """Chỉ inject nếu text giống tiêu đề điều khoản (chữ HOA, ngắn)."""
        clean = re.sub(r"\*{1,3}", "", text).strip()
        if len(clean) < 3 or len(clean) > 150:
            return False
        letters = [c for c in clean if c.isalpha()]
        if not letters:
            return False
        upper_ratio = sum(1 for c in letters if c.isupper()) / len(letters)
        return upper_ratio >= 0.6  # >= 60% chữ HOA → heading

    lines = md.split("\n")
    result = []
    injected = 0

    for line in lines:
        m = _RE_NUMBERED_HEADING.match(line.strip())
        if m:
            prefix = m.group(1) or ""   # "# " or ""
            num = m.group(2)             # "1", "2", ...
            text = m.group(3)            # "ĐỐI TƯỢNG CỦA HỢP ĐỒNG"

            # Kiểm tra không phải sub-heading (1.1, 1.2...)
            if not re.match(r"^\d+\.", text):
                if _should_inject(text):
                    # Inject: "# 1. TEXT" → "# Điều 1. TEXT"
                    heading_prefix = prefix if prefix else "# "
                    result.append(f"{heading_prefix}Điều {num}. {text}")
                    injected += 1
                    continue

        result.append(line)

    if injected > 0:
        logger.info(f"  [INJECT] Đã inject 'Điều' vào {injected} headings")

    return "\n".join(result)


# ─────────────────────────────────────────────────────────────
# Strategy A: docx_converter (raw XML parse)
# Chỉ dùng được cho .docx, KHÔNG dùng cho .doc
# ─────────────────────────────────────────────────────────────

def _strategy_docx_native(file_bytes: bytes, filename: str) -> dict:
    """Parse DOCX trực tiếp bằng docx_converter (numbering.xml → 'Điều %1.')."""
    try:
        result = convert_docx_to_markdown(file_bytes, filename)
        md = result.get("document", {}).get("md_content", "")
        logger.info(
            f"  [A: docx_native] {len(md)} chars, "
            f"{_count_dieu(md)} 'Điều'"
        )
        return {"md": md, "backend": "docx_native"}
    except Exception as e:
        logger.warning(f"  [A: docx_native] Failed: {e}")
        return {"md": "", "backend": "docx_native_failed"}


# ─────────────────────────────────────────────────────────────
# Strategy A2: mammoth (DOCX → HTML → markdown)
# mammoth tự expand list numbering (bao gồm "Điều %1.") thành text
# Chỉ dùng được cho .docx
# ─────────────────────────────────────────────────────────────

def _html_to_markdown(html: str) -> str:
    """Convert HTML fragment from mammoth to clean markdown text."""
    soup = BeautifulSoup(html, "lxml")

    md_lines = []

    for el in soup.find_all(["h1", "h2", "h3", "h4", "h5", "h6", "p", "li", "table"]):
        tag = el.name

        if tag.startswith("h") and len(tag) == 2 and tag[1].isdigit():
            level = int(tag[1])
            text = el.get_text(strip=True)
            if text:
                md_lines.append(f"{'#' * level} {text}")

        elif tag == "p":
            text = el.get_text(strip=True)
            if text:
                md_lines.append(text)

        elif tag == "li":
            text = el.get_text(strip=True)
            if text:
                # Check parent: ol → numbered, ul → bullet
                parent = el.find_parent(["ol", "ul"])
                if parent and parent.name == "ol":
                    # Get index within parent
                    siblings = parent.find_all("li", recursive=False)
                    idx = siblings.index(el) + 1 if el in siblings else 1
                    md_lines.append(f"{idx}. {text}")
                else:
                    md_lines.append(f"- {text}")

        elif tag == "table":
            rows = el.find_all("tr")
            if rows:
                table_data = []
                for row in rows:
                    cells = row.find_all(["td", "th"])
                    table_data.append([c.get_text(strip=True) for c in cells])
                if table_data:
                    max_cols = max(len(r) for r in table_data)
                    for r in table_data:
                        while len(r) < max_cols:
                            r.append("")
                    md_lines.append("| " + " | ".join(table_data[0]) + " |")
                    md_lines.append("| " + " | ".join(["---"] * max_cols) + " |")
                    for r in table_data[1:]:
                        md_lines.append("| " + " | ".join(r) + " |")

    md = "\n\n".join(md_lines)

    # Post-process: apply legal heading detection (same as docx_converter)
    from docx_converter import _postprocess_legal_headings
    md = _postprocess_legal_headings(md)

    return md.strip()


def _strategy_docx_html(file_bytes: bytes, filename: str) -> dict:
    """Convert DOCX → HTML (mammoth) → markdown. mammoth expands numbering."""
    try:
        result = mammoth.convert_to_html(BytesIO(file_bytes))
        html = result.value
        if not html or not html.strip():
            logger.info("  [A2: docx_html] Empty HTML output")
            return {"md": "", "backend": "docx_html_empty"}

        md = _html_to_markdown(html)
        logger.info(
            f"  [A2: docx_html] {len(md)} chars, "
            f"{_count_dieu(md)} 'Điều'"
        )
        return {"md": md, "backend": "docx_html"}
    except Exception as e:
        logger.warning(f"  [A2: docx_html] Failed: {e}")
        return {"md": "", "backend": "docx_html_failed"}


# ─────────────────────────────────────────────────────────────
# Strategy B: Google Drive → export DOCX → docx_converter
# Upload file lên Drive, export ra DOCX, rồi parse bằng docx_converter
# Google Docs có thể rebuild numbering.xml → giữ "Điều"
# Dùng được cho cả .doc và .docx
# ─────────────────────────────────────────────────────────────

def _strategy_drive_docx(file_bytes: bytes, filename: str, content_type: str) -> dict:
    """Drive OCR → export DOCX → docx_converter parse."""
    ext = os.path.splitext(filename)[1].lower() or ".docx"

    with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
        tmp.write(file_bytes)
        tmp_path = tmp.name

    try:
        drive = get_drive_service()
        docx_bytes = ocr_file(
            drive, tmp_path,
            source_mime_type=content_type,
            filename=filename,
            export_mode="docx",
        )
        if not docx_bytes:
            logger.info("  [B: drive_docx] Empty DOCX export")
            return {"md": "", "backend": "drive_docx_empty"}

        result = convert_docx_to_markdown(docx_bytes, filename)
        md = result.get("document", {}).get("md_content", "")
        logger.info(
            f"  [B: drive_docx] {len(md)} chars, "
            f"{_count_dieu(md)} 'Điều'"
        )
        return {"md": md, "backend": "drive_docx"}
    except Exception as e:
        logger.warning(f"  [B: drive_docx] Failed: {e}")
        return {"md": "", "backend": "drive_docx_failed"}
    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)


# ─────────────────────────────────────────────────────────────
# Strategy C: Google Drive → export text → text_to_markdown
# Plain text export, rồi regex detect "ĐIỀU X." heading
# Dùng được cho cả .doc, .docx, .pdf, image
# ─────────────────────────────────────────────────────────────

def _strategy_drive_text(file_bytes: bytes, filename: str, content_type: str) -> dict:
    """Drive OCR → export text → regex-based heading detection."""
    ext = os.path.splitext(filename)[1].lower() or ".docx"

    with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
        tmp.write(file_bytes)
        tmp_path = tmp.name

    try:
        drive = get_drive_service()
        raw_text = ocr_file(
            drive, tmp_path,
            source_mime_type=content_type,
            filename=filename,
            export_mode="text",
        )
        if not raw_text:
            logger.info("  [C: drive_text] Empty text export")
            return {"md": "", "backend": "drive_text_empty"}

        from text_to_markdown import text_to_markdown
        md = text_to_markdown(raw_text)
        logger.info(
            f"  [C: drive_text] {len(md)} chars, "
            f"{_count_dieu(md)} 'Điều'"
        )
        return {"md": md, "backend": "drive_text"}
    except Exception as e:
        logger.warning(f"  [C: drive_text] Failed: {e}")
        return {"md": "", "backend": "drive_text_failed"}
    finally:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)


# ─────────────────────────────────────────────────────────────
# Main conversion logic
# ─────────────────────────────────────────────────────────────

def _convert_document(file_bytes: bytes, filename: str, content_type: str) -> dict:
    """
    Chạy tất cả strategies phù hợp, chọn output nhiều "Điều" nhất.

    DOCX → chạy 4 strategy (A + A2 + B + C)
    DOC  → chạy 2 strategy (B + C), vì python-docx không đọc .doc
    """
    ext = os.path.splitext(filename)[1].lower()

    # ── Fix: detect ext từ content_type nếu filename không có extension ──
    if not ext and content_type:
        ext = MIME_TO_EXT.get(content_type, "")
        if ext:
            logger.info(f"  [EXT] Filename '{filename}' không có extension, detect từ content_type → ext={ext}")

    if not content_type:
        content_type = MIME_MAP.get(ext, "application/octet-stream")

    logger.info(
        f"Converting: {filename} ({len(file_bytes)} bytes, "
        f"ext={ext}, content_type={content_type})"
    )

    is_docx = ext == ".docx"
    is_doc = ext == ".doc"
    candidates = []

    # ── Strategy A: docx_converter (chỉ .docx) ──
    if is_docx:
        logger.info("  [STRATEGY A: docx_native] Running — parse XML numbering.xml...")
        result_a = _strategy_docx_native(file_bytes, filename)
        candidates.append(result_a)

        # Nếu Strategy A đã có "Điều" → skip tất cả
        if _count_dieu(result_a["md"]) > 0 and _meaningful_chars(result_a["md"]) > 200:
            logger.info(f"  ✅ Strategy A có {_count_dieu(result_a['md'])} Điều → DONE, skip A2/B/C")
            best = result_a
            return _build_response(filename, best, candidates)
        else:
            logger.info(f"  ❌ Strategy A: {_count_dieu(result_a['md'])} Điều → thử A2...")

        # ── Strategy A2: mammoth DOCX → HTML → markdown ──
        logger.info("  [STRATEGY A2: docx_html] Running — mammoth DOCX→HTML→MD...")
        result_a2 = _strategy_docx_html(file_bytes, filename)
        candidates.append(result_a2)

        # Nếu A2 đã có "Điều" → skip Drive calls
        if _count_dieu(result_a2["md"]) > 0 and _meaningful_chars(result_a2["md"]) > 200:
            logger.info(f"  ✅ Strategy A2 có {_count_dieu(result_a2['md'])} Điều → DONE, skip B/C")
            best = _pick_best(*candidates)
            return _build_response(filename, best, candidates)
        else:
            logger.info(f"  ❌ Strategy A2: {_count_dieu(result_a2['md'])} Điều → thử B/C (Drive)...")
    else:
        logger.info(f"  [SKIP A, A2] ext={ext} không phải .docx → skip offline strategies")

    # ── Strategy B: Drive → DOCX → docx_converter ──
    logger.info("  [STRATEGY B: drive_docx] Running — upload Drive → export DOCX → parse...")
    result_b = _strategy_drive_docx(file_bytes, filename, content_type)
    candidates.append(result_b)

    # ── Strategy C: Drive → text → regex ──
    logger.info("  [STRATEGY C: drive_text] Running — upload Drive → export text → regex...")
    result_c = _strategy_drive_text(file_bytes, filename, content_type)
    candidates.append(result_c)

    # ── Chọn best ──
    best = _pick_best(*candidates)
    return _build_response(filename, best, candidates)


def _build_response(filename: str, best: dict, candidates: list) -> dict:
    """Build API response từ best candidate."""
    md = best["md"]
    backend = best["backend"]

    # ── Last-resort: inject "Điều" nếu không strategy nào bắt được ──
    if md and _count_dieu(md) == 0:
        logger.info("  Không có Điều nào → thử inject...")
        md = _inject_dieu(md)
        if _count_dieu(md) > 0:
            backend = f"{backend}+inject"

    dieu_per_strategy = {
        c["backend"]: _count_dieu(c["md"])
        for c in candidates if c["md"]
    }

    logger.info(
        f"  [RESULT] backend={backend}, "
        f"dieu={_count_dieu(md)}, chars={len(md)}, "
        f"all_strategies={dieu_per_strategy}"
    )

    return {
        "document": {
            "filename": filename,
            "md_content": md,
        },
        "meta": {
            "backend": backend,
            "low_information": _looks_like_low_information(md),
            "dieu_count": _count_dieu(md),
            "strategies_tried": dieu_per_strategy,
        },
    }


# ─────────────────────────────────────────────────────────────
# API Endpoint
# ─────────────────────────────────────────────────────────────

@app.post("/v1/convert/file")
async def convert_file(files: UploadFile = File(...)):
    """
    Convert uploaded DOCX/DOC file to markdown.
    Tự động chọn strategy tốt nhất để giữ "Điều".
    """
    filename = files.filename or "unknown"
    ext = os.path.splitext(filename)[1].lower() or ".bin"
    file_bytes = await files.read()

    logger.info(
        f"Received: {filename} ({len(file_bytes)} bytes, "
        f"ext={ext}, type={files.content_type})"
    )

    if not file_bytes:
        return JSONResponse(
            status_code=400,
            content={"error": "Empty file upload."},
        )

    try:
        response = _convert_document(file_bytes, filename, files.content_type)

        if not response.get("document", {}).get("md_content"):
            response["warning"] = "Could not extract text from this file."

        return JSONResponse(content=response)

    except Exception as e:
        logger.error(f"Conversion failed: {str(e)}")
        return JSONResponse(
            status_code=500,
            content={"error": f"Conversion failed: {str(e)}"},
        )
