"""
📄 utils.py

Purpose:
    Generic helper functions reused across multiple modules.

Key Features:
    - clean_text(): Whitespace normalization.
    - get_snippet_around_match(): Provides contextual text around regex matches.
    - format_tags(): Pretty output of tags for terminal view.

Used by:
    - All search modes and export utilities.
"""
import re
from textwrap import wrap
from fpdf.errors import FPDFException
from rich.text import Text
from .db_utils import get_tags_for_file
from nltk.tokenize import sent_tokenize


# print("DEBUG: updated hightlight_term loaded")
def highlight_term(text, term):
    """
    Highlight all occurrences of `term` in `text` using Rich Text.
    Matches appear in bold red; surrounding text can have a different style.
    """
    if not term:
        return Text(text)  # plain Text if nothing to highlight

    t = Text(text)  # base Text object
    pattern = re.compile(rf"({re.escape(term)})", re.IGNORECASE)

    for match in pattern.finditer(text):
        start, end = match.span()
        t.stylize("bold red", start, end)

    return t


def format_tags(path):
    
    tags = get_tags_for_file(path)
    if not tags:
        return ""
    # Parse tags and sort
    tag_dict = {}
    for tag in tags:
        if ":" in tag:
            key, val = tag.split(":", 1)
            tag_dict[key.strip()] = val.strip()
    sorted_pairs = [f"{k}: {v}" for k, v in sorted(tag_dict.items())]
    return f"[{' | '.join(sorted_pairs)}]"


def get_snippets(text, query, context_sentences=2):
    text = re.sub(r"\s+", " ", text).strip()
    sentences = sent_tokenize(text)
    matches = []

    for i, sent in enumerate(sentences):
        if re.search(re.escape(query), sent, re.IGNORECASE):
            start = max(0, i - context_sentences)
            end = min(len(sentences), i + context_sentences + 1)
            snippet = " ".join(sentences[start:end])
            highlighted = re.sub(
                f"({re.escape(query)})",
                f"\033[93m\1\033[0m",
                snippet,
                flags=re.IGNORECASE,
            )
            matches.append(highlighted.strip())

    return matches


def print_with_context(file_path, paragraph, term, paragraph_num=0):
    lines = paragraph.strip().splitlines()
    context = "\n".join(line.strip() for line in lines if term.lower() in line.lower())

    if not context.strip():
        return  # Skip empty matches

    print(f"\n📄 {file_path}")
    print(highlight_term(context.strip(), term))


def clean_text(text):
    text = re.sub(r"<[^>]+>", " ", text)  # Strip HTML tags
    text = re.sub(r"\s+", " ", text)  # Normalize whitespace
    text = re.sub(r"[\x00-\x1F\x7F-\x9F]", "", text)  # Remove control chars
    return text.strip()

def clean_tags(tags):
    cleaned = []
    for tag in tags:
        if not tag:
            continue
        tag = tag.strip().replace(";", ":")  # normalize key-value separator
        tag = " ".join(tag.split())  # collapse multiple spaces
        cleaned.append(tag)
    return list(dict.fromkeys(cleaned))  # remove duplicates, preserve order

def get_snippet_around_match(text, match_obj, context_chars):
    start, end = match_obj.start(), match_obj.end()
    # Ensure snippet length is up to context_chars, with match roughly centered
    half_context = context_chars // 2
    snippet_start = max(0, start - half_context)
    snippet_end = min(len(text), end + half_context)
    
    # If snippet shorter than context_chars, expand to full length if possible
    if snippet_end - snippet_start < context_chars:
        if snippet_start == 0:
            snippet_end = min(len(text), snippet_start + context_chars)
        elif snippet_end == len(text):
            snippet_start = max(0, snippet_end - context_chars)

    return clean_text(text[snippet_start:snippet_end])

def build_snippet(content, terms, context_chars=150, fuzzy=False, max_snippets=3):
    """
    Wrapper for snippet building:
    - Uses multi-term merge for exact matches.
    - Uses single-term centered snippet for fuzzy fallback.
    """
    if fuzzy:
        # Take first term and search for approximate match
        term = terms[0] if terms else ""
        if not term:
            return ""
        match = re.search(re.escape(term), content, re.IGNORECASE)
        if match:
            return get_snippet_around_match(content, match, context_chars)
        # fallback: truncated slice
        return clean_text(content[:context_chars])
    else:
        return get_snippets_for_matches(content, terms, context_chars, max_snippets)


def get_snippets_for_matches(
    content: str,
    search_terms: list[str],
    context_chars: int = 150,
    max_snippets: int = 3,
) -> str:
    match_ranges = []

    # Collect all match ranges
    for term in search_terms:
        if not term:
            continue
        for match in re.finditer(re.escape(term), content, re.IGNORECASE):
            start, end = match.start(), match.end()
            half_ctx = context_chars // 2
            start = max(0, start - half_ctx)
            end = min(len(content), end + half_ctx)
            match_ranges.append((start, end))

    if not match_ranges:
        return ""

    # Merge overlapping ranges
    match_ranges.sort()
    merged_ranges = [match_ranges[0]]
    for current in match_ranges[1:]:
        last = merged_ranges[-1]
        if current[0] <= last[1]:
            merged_ranges[-1] = (last[0], max(last[1], current[1]))
        else:
            merged_ranges.append(current)

    # Extract merged snippets
    snippets = [clean_text(content[s:e]) for s, e in merged_ranges[:max_snippets]]

    # 🔍 Debug: show applied context length
    # print(f"[DEBUG] context_chars={context_chars}, snippet_lengths={[len(sn) for sn in snippets]}")

    return " ... ".join(snippets)


# Adding pdf export support

def wrap_text_fpdf(text, width=80):
    return wrap(text, width=width, break_long_words=True, break_on_hyphens=False)


def safe_text(text):
    try:
        # Remove ANSI escape sequences
        text = re.sub(r"\x1B\[[0-?]*[ -/]*[@-~]", "", str(text))
        # Remove other control characters (non-printable)
        text = "".join(c for c in text if c.isprintable())
        return text.encode("latin-1", "replace").decode("latin-1")
    except:
        return str(text)[:300]


def _soft_breaks(text: str) -> str:
    """Insert soft break opportunities so long paths can wrap."""
    zwsp = "\u200b"
    return (text
            .replace("/", f"/{zwsp}")
            .replace("\\", f"\\{zwsp}")
            .replace("_", f"_{zwsp}")
            .replace("-", f"-{zwsp}")
            .replace(".", f".{zwsp}"))


def _safe_multicell(pdf, text: str, h: float = 8):
    """Always render from left margin with a positive width and catch width errors."""
    if not text:
        return
    text = _soft_breaks(text)
    try:
        # Effective page width (fpdf2 provides `epw`)
        w = getattr(pdf, "epw", pdf.w - pdf.l_margin - pdf.r_margin)
        pdf.set_x(pdf.l_margin)              # start from left margin
        pdf.multi_cell(w, h, text)
    except FPDFException as e:
        # Retry with a smaller font once
        try:
            old = pdf.font_size_pt
            pdf.set_font_size(max(6, old - 2))
            pdf.set_x(pdf.l_margin)
            pdf.multi_cell(w, h, text)
            pdf.set_font_size(old)           # restore
        except Exception as e2:
            print(f"⚠️ Skipped line: {e2}")
