"""CV visual quality checker — diagnoses orphans, overfull/underfull hbox, page fill.

Analyzes a compiled LaTeX CV (PDF + log) for visual issues:
- Orphan lines (short trailing lines in paragraphs)
- Overfull/underfull hbox warnings from pdflatex
- Page fill percentage
- Sidebar hyphenations
- Page count validation

Usage as module:
    from cv4offer.checker import check_cv
    result = check_cv("path/to/cv.tex")

Usage as CLI:
    cv4offer check path/to/cv.tex
"""

import json
import re
import sys
from pathlib import Path

import fitz  # PyMuPDF


# Default layout constants (pt) for paracol 0.30/0.70 split with 1cm columnsep
DEFAULT_LAYOUT = {
    "sidebar_x_max": 190.0,
    "main_x_min": 200.0,
    "main_column_width": 350.0,
}

# Thresholds
ORPHAN_WARNING_PCT = 15.0
ORPHAN_INFO_PCT = 40.0
PAGE_FILL_WARNING = 85.0

# Section titles (both PL and EN) that are short by design
SECTION_TITLES = {
    "Podsumowanie zawodowe", "Wybrane osiągnięcia", "Doświadczenie zawodowe",
    "Umiejętności", "Wykształcenie", "Zainteresowania", "Rekomendacje",
    "Professional Summary", "Key Achievements", "Work Experience",
    "Skills", "Education", "Interests", "Recommendations", "Summary",
    "Experience", "Projects", "Certifications", "Languages",
}

IGNORE_TEXTS = {"✓", "•", "–", "—", "-"}


def find_area_for_line(tex_lines: list[str], line_num: int) -> str:
    """Determine if a .tex line number is in sidebar (leftcolumn) or main (rightcolumn)."""
    for i in range(line_num - 1, -1, -1):
        line = tex_lines[i] if i < len(tex_lines) else ""
        if r"\begin{rightcolumn}" in line:
            return "main"
        if r"\begin{leftcolumn}" in line:
            return "sidebar"
    return "unknown"


def parse_log_warnings(log_path: Path, tex_lines: list[str]) -> list[dict]:
    """Parse pdflatex .log for overfull/underfull hbox warnings."""
    issues = []
    if not log_path.exists():
        return issues

    log_text = log_path.read_text(encoding="utf-8", errors="replace")
    lines = log_text.split("\n")
    current_page = 1

    for i, line in enumerate(lines):
        page_matches = re.findall(r"\[(\d+)", line)
        for pm in page_matches:
            current_page = int(pm)

        m = re.match(
            r"Overfull \\hbox \((\d+\.?\d*)pt too wide\) in paragraph at lines (\d+)--(\d+)",
            line,
        )
        if m:
            excess = float(m.group(1))
            line_start = int(m.group(2))
            area = find_area_for_line(tex_lines, line_start)
            text_snippet = ""
            for j in range(i + 1, min(i + 3, len(lines))):
                if lines[j].strip() and lines[j].strip() != "[]":
                    text_snippet = re.sub(r"\\T1/[^ ]+ ", "", lines[j]).strip(" []")
                    break
            issues.append({
                "severity": "WARNING",
                "type": "overfull_hbox",
                "page": current_page,
                "line": line_start,
                "excess_pt": round(excess, 2),
                "area": area,
                "text": text_snippet[:80],
            })
            continue

        m = re.match(
            r"Underfull \\hbox \(badness (\d+)\) in paragraph at lines (\d+)--(\d+)",
            line,
        )
        if m:
            badness = int(m.group(1))
            line_start = int(m.group(2))
            area = find_area_for_line(tex_lines, line_start)
            text_snippet = ""
            for j in range(i + 1, min(i + 3, len(lines))):
                if lines[j].strip() and lines[j].strip() != "[]":
                    text_snippet = re.sub(r"\\T1/[^ ]+ ", "", lines[j]).strip(" []")
                    break
            severity = "INFO"
            if area == "main" and badness >= 5000:
                severity = "WARNING"
            issues.append({
                "severity": severity,
                "type": "underfull_hbox",
                "page": current_page,
                "line": line_start,
                "badness": badness,
                "area": area,
                "text": text_snippet[:80],
            })
            continue

    return issues


def is_false_orphan(text: str, extra_patterns: list[str] | None = None) -> bool:
    """Check if a short line is actually a section title, date, or other non-orphan."""
    text_clean = text.strip()
    if text_clean in IGNORE_TEXTS:
        return True
    if len(text_clean) <= 2:
        return True
    if text_clean in SECTION_TITLES:
        return True

    base_patterns = [
        r"^\d{4}$",
        r"^\d{4}\s*[-–—]\s*(\d{4}|\w+)",  # date ranges
        r"→",
    ]
    all_patterns = base_patterns + (extra_patterns or [])
    for pattern in all_patterns:
        if re.search(pattern, text_clean):
            return True
    return False


def analyze_pdf(
    pdf_path: Path,
    layout: dict | None = None,
    expected_pages: int = 2,
) -> dict:
    """Analyze PDF for page count, fill, and orphans using PyMuPDF."""
    cfg = {**DEFAULT_LAYOUT, **(layout or {})}
    doc = fitz.open(str(pdf_path))
    result = {
        "pages": len(doc),
        "expected_pages": expected_pages,
        "page_fill": [],
        "orphans": [],
        "sidebar_hyphenations": 0,
    }

    for page_num in range(len(doc)):
        page = doc[page_num]
        page_rect = page.rect
        usable_top = 50.0
        usable_bottom = page_rect.height - 40.0
        usable_height = usable_bottom - usable_top

        blocks = page.get_text("dict", flags=fitz.TEXT_PRESERVE_WHITESPACE)["blocks"]

        max_y = usable_top
        for block in blocks:
            if block["type"] != 0:
                continue
            for bline in block["lines"]:
                y1 = bline["bbox"][3]
                if usable_top < y1 <= usable_bottom:
                    max_y = max(max_y, y1)

        fill_pct = round(((max_y - usable_top) / usable_height) * 100, 1)
        result["page_fill"].append(min(fill_pct, 100.0))

        prev_line_info = None
        for block in blocks:
            if block["type"] != 0:
                continue
            for bline in block["lines"]:
                bbox = bline["bbox"]
                x0, y0, x1, y1 = bbox
                line_width = x1 - x0

                text = "".join(span["text"] for span in bline["spans"]).strip()
                if not text:
                    continue

                if x0 < cfg["sidebar_x_max"] and "-" in text and text.endswith("-"):
                    result["sidebar_hyphenations"] += 1

                if x0 < cfg["main_x_min"]:
                    prev_line_info = {"y": y1, "x0": x0, "width": line_width, "text": text}
                    continue

                width_pct = round((line_width / cfg["main_column_width"]) * 100, 1)

                if width_pct < ORPHAN_INFO_PCT and prev_line_info is not None:
                    if not is_false_orphan(text):
                        severity = "WARNING" if width_pct < ORPHAN_WARNING_PCT else "INFO"
                        result["orphans"].append({
                            "severity": severity,
                            "type": "orphan_line",
                            "page": page_num + 1,
                            "y": round(y0),
                            "text": text[:60],
                            "width_pct": width_pct,
                            "area": "main",
                        })

                prev_line_info = {"y": y1, "x0": x0, "width": line_width, "text": text}

    doc.close()
    return result


def check_cv(
    tex_path: str | Path,
    expected_pages: int = 2,
    layout: dict | None = None,
    extra_false_orphan_patterns: list[str] | None = None,
) -> dict:
    """Run full CV quality check. Returns dict with issues and summary.

    Args:
        tex_path: Path to .tex file (PDF and .log must exist alongside it).
        expected_pages: Expected number of pages (default 2).
        layout: Override layout constants (sidebar_x_max, main_x_min, main_column_width).
        extra_false_orphan_patterns: Additional regex patterns for false orphan detection
            (e.g. company names, city names specific to your CV).
    """
    tex_path = Path(tex_path)
    pdf_path = tex_path.with_suffix(".pdf")
    log_path = tex_path.with_suffix(".log")

    if not tex_path.exists():
        return {"error": f"File not found: {tex_path}"}
    if not pdf_path.exists():
        return {"error": f"PDF not found: {pdf_path}. Compile first with pdflatex."}

    tex_lines = tex_path.read_text(encoding="utf-8").split("\n")

    pdf_data = analyze_pdf(pdf_path, layout=layout, expected_pages=expected_pages)
    log_issues = parse_log_warnings(log_path, tex_lines)

    all_issues = pdf_data["orphans"] + log_issues

    if pdf_data["pages"] != expected_pages:
        all_issues.insert(0, {
            "severity": "CRITICAL",
            "type": "page_count",
            "pages": pdf_data["pages"],
            "text": f"Expected {expected_pages} pages, got {pdf_data['pages']}",
        })

    for i, fill in enumerate(pdf_data["page_fill"]):
        if fill < PAGE_FILL_WARNING:
            all_issues.append({
                "severity": "WARNING",
                "type": "page_fill",
                "page": i + 1,
                "fill_pct": fill,
                "text": f"Page {i + 1} only {fill}% filled",
            })

    summary = {"critical": 0, "warning": 0, "info": 0}
    for issue in all_issues:
        sev = issue["severity"].lower()
        if sev in summary:
            summary[sev] += 1

    return {
        "file": tex_path.name,
        "pages": pdf_data["pages"],
        "page_fill": pdf_data["page_fill"],
        "sidebar_hyphenations": pdf_data["sidebar_hyphenations"],
        "issues": all_issues,
        "summary": summary,
    }


def main():
    """CLI entry point for standalone checker usage."""
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <path/to/cv_file.tex>", file=sys.stderr)
        sys.exit(3)

    result = check_cv(sys.argv[1])

    if "error" in result:
        print(json.dumps(result))
        sys.exit(3)

    print(json.dumps(result, indent=2, ensure_ascii=False))

    if result["summary"]["critical"] > 0:
        sys.exit(2)
    elif result["summary"]["warning"] > 0:
        sys.exit(1)
    else:
        sys.exit(0)


if __name__ == "__main__":
    main()
