"""Best-effort extraction of Job Description fields from an uploaded PDF/DOCX/TXT.

The parser is *section-aware*: single-line "Label: value" fields are read
individually, while free-form blocks (Work Details / Responsibilities,
Qualifications, Skills, Screening Q&A) are detected by their heading and
captured with their original structure — paragraphs, bullet points, numbered
lists and line breaks are preserved rather than flattened into one blob.
Template boilerplate (helper text, bracketed placeholders) is ignored so only
the user-entered content is parsed.
"""
import html
import os
import re

from pdfminer.high_level import extract_text as _pdf_text

try:
    from docx import Document
except ImportError:
    Document = None


# A bullet / numbered list marker at the start of a line, e.g. "- ", "• ", "1. ".
_LIST_RE = re.compile(r"^\s*([-*•·▪◦]|\d+[.)])\s+(.*)$")


def extract_text(file_path: str) -> str:
    """Return the document's text with line/paragraph structure preserved.

    For DOCX we additionally surface list items with an explicit "• " marker so
    bullet formatting survives into the plain-text pipeline (python-docx does not
    render list markers into ``paragraph.text``).
    """
    ext = os.path.splitext(file_path)[1].lower()
    if ext == ".pdf":
        return _pdf_text(file_path) or ""
    if ext in (".docx",) and Document:
        d = Document(file_path)
        parts = []
        for par in d.paragraphs:
            txt = par.text or ""
            style = (par.style.name or "").lower() if par.style is not None else ""
            # Re-attach a visible marker for styled list items that have none.
            if txt.strip() and ("list bullet" in style or "list number" in style):
                if not _LIST_RE.match(txt):
                    txt = "• " + txt.strip()
            parts.append(txt)
        # include table cells as "label: value" so table-based templates parse too
        for tbl in d.tables:
            for row in tbl.rows:
                cells = [c.text.strip() for c in row.cells]
                if len(cells) >= 2 and cells[0]:
                    parts.append(f"{cells[0]}: {cells[1]}")
                elif cells:
                    parts.append(" ".join(cells))
        return "\n".join(parts)
    if ext in (".txt",):
        with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
            return fh.read()
    return ""


# Single-line "Label: value" fields, matched case-insensitively as a full line.
_LABELS = [
    (r"job\s*title|position|role|title", "title"),
    (r"department|dept", "department"),
    (r"location|work\s*location|city", "location"),
    (r"experience|exp\.?|experience\s*band", "experience_band"),
    (r"ctc|salary|compensation|package|ctc\s*band", "ctc_band"),
    (r"notice\s*period", "notice_period"),
    (r"shift", "shift"),
    (r"qualification[s]?|education|educational\s*qualification", "qualifications"),
    (r"working\s*days|work\s*days", "working_days"),
    (r"no\.?\s*of\s*positions?|number\s*of\s*positions?|positions?|openings?|vacanc(?:y|ies)", "num_positions"),
    (r"certifications?", "certification"),
    (r"must[-\s]*have\s*skills|required\s*skills|mandatory\s*skills", "must_have_skills"),
    (r"good[-\s]*to[-\s]*have\s*skills|preferred\s*skills|nice[-\s]*to[-\s]*have", "good_to_have_skills"),
]

# Block-section headings (a heading on its own line, optionally trailed by ':').
# Order matters: the first pattern that matches wins.
_SECTION_HEADINGS = [
    (r"work\s*details(?:\s*&?\s*requirements?)?", "work_details"),
    (r"job\s*description|role\s*description|about\s*the\s*role|job\s*summary|summary", "work_details"),
    (r"roles?\s*&?\s*responsibilit(?:y|ies)|key\s*responsibilit(?:y|ies)|responsibilit(?:y|ies)|duties", "work_details"),
    (r"requirements?|what\s*you.?ll\s*do", "work_details"),
    (r"qualifications?|educational?\s*qualifications?|education", "qualifications"),
    (r"must[-\s]*have\s*skills|required\s*skills|mandatory\s*skills|key\s*skills", "must_have_skills"),
    (r"good[-\s]*to[-\s]*have\s*skills|preferred\s*skills|nice[-\s]*to[-\s]*have\s*skills", "good_to_have_skills"),
    (r"screening\s*questions?(?:\s*&?\s*answers?)?|interview\s*questions?|questions?\s*&\s*answers?", "questions"),
]

# Exact template boilerplate / helper lines to ignore (case-insensitive, trimmed).
_PLACEHOLDER_EXACT = {
    "write the full role description, responsibilities and requirements here.",
    "write the full role description, responsibilities and requirements here",
}

# Instruction-style helper lines, e.g. "Enter the responsibilities below" —
# targeted so real content is never dropped.
_PLACEHOLDER_RE = re.compile(
    r"^\s*(write|enter|provide|describe|type|list|mention|specify|fill\s*in|add\s*your)\b.*\b(here|below)\s*[.:]?\s*$",
    re.I,
)


def _is_placeholder(line: str) -> bool:
    s = line.strip()
    if not s:
        return False
    low = s.lower()
    if low in _PLACEHOLDER_EXACT:
        return True
    # Bracketed placeholders: [ ... ], < ... >, {{ ... }}
    if (s.startswith("[") and s.endswith("]")) or (s.startswith("<") and s.endswith(">")) \
            or (s.startswith("{{") and s.endswith("}}")):
        return True
    if low.startswith("e.g."):
        return True
    return bool(_PLACEHOLDER_RE.match(s))


def _match_section(label: str):
    """Return the target field for a heading line, or None."""
    stripped = label.strip().rstrip(":").strip()
    for pattern, field in _SECTION_HEADINGS:
        if re.fullmatch(rf"\s*({pattern})\s*", stripped, re.I):
            return field
    return None


def _is_label_line(line: str) -> bool:
    """True for a single-line "Label: value" field (title/department/…)."""
    if ":" not in line:
        return False
    label = line.partition(":")[0].strip().lower()
    return any(
        re.fullmatch(rf"\s*({pattern})\s*", label)
        for pattern, _field in _LABELS
    )


def _lines_to_html(lines) -> str:
    """Render captured section lines as clean HTML, preserving paragraphs and
    bullet / numbered lists. ``work_details`` is stored and displayed as HTML
    (rich-text editor + dangerouslySetInnerHTML), so structure survives."""
    # Trim leading/trailing blank lines.
    buf = list(lines)
    while buf and not buf[0].strip():
        buf.pop(0)
    while buf and not buf[-1].strip():
        buf.pop()
    if not buf:
        return ""

    out = []
    list_items = []
    list_tag = None

    def flush_list():
        nonlocal list_items, list_tag
        if list_items:
            lis = "".join(f"<li>{html.escape(it)}</li>" for it in list_items)
            out.append(f"<{list_tag}>{lis}</{list_tag}>")
            list_items = []
            list_tag = None

    for ln in buf:
        m = _LIST_RE.match(ln)
        if m:
            marker, content = m.group(1), m.group(2).strip()
            tag = "ol" if re.match(r"^\d", marker) else "ul"
            if list_tag and list_tag != tag:
                flush_list()
            list_tag = tag
            list_items.append(content)
        elif not ln.strip():
            flush_list()
        else:
            flush_list()
            out.append(f"<p>{html.escape(ln.strip())}</p>")
    flush_list()
    return "".join(out)


def _clean_block_text(lines) -> str:
    """Plain-text join for non-HTML block fields (qualifications), preserving
    line breaks and stripping any list markers."""
    cleaned = []
    for ln in lines:
        m = _LIST_RE.match(ln)
        cleaned.append(m.group(2).strip() if m else ln.strip())
    text = "\n".join(cleaned).strip()
    return text


def _clean_skills(lines) -> str:
    """Skills blocks display as comma-split chips — flatten list items to a
    comma-separated string, keeping any commas the author already used."""
    items = []
    for ln in lines:
        m = _LIST_RE.match(ln)
        val = (m.group(2) if m else ln).strip()
        if not val:
            continue
        items.extend([p.strip() for p in val.split(",") if p.strip()])
    return ", ".join(items)


def parse_jd(text: str) -> dict:
    """Return a dict of best-effort field values. Unknown fields are left blank.

    Backward-compatible: if no explicit "Work Details" heading is found, the
    free-form content (everything that is not a recognised label line or
    placeholder) still becomes ``work_details`` — so non-templated documents
    keep working exactly as before, only without the label lines leaking in.
    """
    out = {
        "title": "", "department": "", "location": "", "experience_band": "",
        "ctc_band": "", "notice_period": "", "shift": "", "qualifications": "",
        "working_days": "", "num_positions": "", "certification": "",
        "must_have_skills": "", "good_to_have_skills": "", "questions": [],
        "work_details": "",
    }

    raw_lines = (text or "").splitlines()

    # Per-section content buffers (raw lines, formatting preserved).
    sections = {"work_details": [], "qualifications": [], "must_have_skills": [], "good_to_have_skills": [], "questions": []}
    # Content seen before any heading — treated as loose work-details prose.
    loose = []
    current = None  # active block section, or None

    for raw in raw_lines:
        ln = raw.rstrip()
        stripped = ln.strip()

        # 1) Blank line — preserve as a paragraph break inside the active block.
        if not stripped:
            if current:
                sections[current].append("")
            elif loose:
                loose.append("")
            continue

        # 2) Section heading? (switches the active block; heading itself dropped)
        section = _match_section(stripped)
        if section:
            current = section
            continue

        # 3) Single-line "Label: value" field.
        matched_field = None
        if ":" in ln:
            label, _, value = ln.partition(":")
            label_norm = label.strip().lower()
            value = value.strip()
            if value and not _is_placeholder(value):
                for pattern, field in _LABELS:
                    if re.fullmatch(rf"\s*({pattern})\s*", label_norm):
                        matched_field = field
                        if not out[field]:
                            out[field] = value
                        break
            if matched_field:
                # A label line also ends the current free-form block.
                current = None
                continue

        # 4) Placeholder / helper text — ignore.
        if _is_placeholder(ln):
            continue

        # 5) Ordinary content line — assign to the active block (or loose prose).
        if current:
            sections[current].append(ln)
        else:
            loose.append(ln)

    # ---- Assemble block fields ----
    # work_details: explicit section first, then loose prose as a fallback.
    wd_lines = sections["work_details"] or loose
    out["work_details"] = _lines_to_html(wd_lines)

    # Block versions only fill a field the inline label didn't already provide.
    if not out["qualifications"] and sections["qualifications"]:
        out["qualifications"] = _clean_block_text(sections["qualifications"])
    if not out["must_have_skills"] and sections["must_have_skills"]:
        out["must_have_skills"] = _clean_skills(sections["must_have_skills"])
    if not out["good_to_have_skills"] and sections["good_to_have_skills"]:
        out["good_to_have_skills"] = _clean_skills(sections["good_to_have_skills"])

    # Last-resort fallback: never lose the document body. If nothing mapped to
    # work_details, keep the whole cleaned text (previous behaviour).
    if not out["work_details"].strip():
        fallback = [
            ln for ln in raw_lines
            if ln.strip() and not _is_placeholder(ln)
            and not _is_label_line(ln) and not _match_section(ln)
        ]
        out["work_details"] = _lines_to_html(fallback)

    # shift -> normalise to a known choice if possible
    low = out["shift"].lower()
    for opt in ("day", "night", "rotational", "flexible"):
        if opt in low:
            out["shift"] = opt.capitalize()
            break

    # num_positions -> keep digits only (e.g. "3 openings" -> "3")
    if out["num_positions"]:
        digits = re.sub(r"\D", "", out["num_positions"])
        out["num_positions"] = digits

    # title fallback: first meaningful, non-heading, non-placeholder line.
    if not out["title"]:
        for raw in raw_lines:
            s = raw.strip()
            if not s or _match_section(s) or _is_placeholder(s):
                continue
            out["title"] = s[:150]
            break

    # ---- Screening questions & answers ----
    # Prefer a dedicated "Screening Questions" section; otherwise scan the whole
    # document for "Q:/A:" pairs (backward compatible).
    qa_source = "\n".join(sections["questions"]) if sections["questions"] else (text or "")
    out["questions"] = parse_qa(qa_source)[:20]

    return out


def parse_qa(text: str) -> list:
    """Extract screening Q&A pairs from a filled Q&A template (QA upload flow).

    Recognises lines prefixed "Q:" / "Q1:" / "Question 1:" and "A:" / "Ans:" /
    "Answer:". A bare line ending in "?" also starts a new question. Unprefixed
    lines are treated as continuations of the current question/answer, so
    multi-line answers survive PDF/DOCX line wrapping.
    """
    q_pat = re.compile(r"^(?:q\d*|question\s*\d*)\s*[:.\)-]\s*(.+)$", re.I)
    a_pat = re.compile(r"^(?:a|ans|answer)\s*[:.\)-]\s*(.+)$", re.I)

    questions = []
    current = None   # {"question": str, "answer": str}
    mode = None      # "q" | "a"

    def flush():
        nonlocal current
        if current and current["question"].strip():
            questions.append({
                "question": current["question"].strip(),
                "answer": current["answer"].strip(),
            })
        current = None

    for raw in (text or "").splitlines():
        ln = raw.strip()
        if not ln:
            continue
        mq = q_pat.match(ln)
        ma = a_pat.match(ln)
        if mq:
            flush()
            current = {"question": mq.group(1).strip(), "answer": ""}
            mode = "q"
        elif ma and current:
            current["answer"] = (current["answer"] + " " + ma.group(1).strip()).strip()
            mode = "a"
        elif ln.endswith("?") and (current is None or mode == "q"):
            flush()
            current = {"question": ln, "answer": ""}
            mode = "q"
        elif current and mode == "q":
            current["question"] = (current["question"] + " " + ln).strip()
        elif current and mode == "a":
            current["answer"] = (current["answer"] + " " + ln).strip()
    flush()
    return questions[:50]
