"""
scrylo/scrapers/card_parser.py
-------------------------------
Pillow pre-processing + regex parser for business card OCR text.
No LLM required for standard cards. Falls back to LLM only when
a person name cannot be confidently extracted.

Pipeline:
  raw image path
    → preprocess_card_image() — Pillow upscale, invert dark cards,
                                 contrast/sharpness boost, binarise
    → EasyOCR on cleaned image
    → extract email, phone, website (regex — deterministic)
    → extract role (known titles list)
    → extract location (city/country patterns)
    → extract name (title-case heuristic, with LLM fallback)
    → return structured dict
"""

from __future__ import annotations

import re
import tempfile
import os
from typing import Optional


# ── Image pre-processing ──────────────────────────────────────────────────────

def preprocess_card_image(image_path: str) -> str:
    """
    Apply Pillow pre-processing to a business card image before EasyOCR.
    Returns path to a cleaned temp file (caller must delete it).

    Steps:
    1. Convert RGBA/P → RGB
    2. Upscale to min 1200px on longest side (LANCZOS)
    3. Convert to greyscale
    4. Detect dark-background cards (mean pixel < 128) → invert
    5. Boost contrast (2.2×) and sharpness (2.0×)
    6. Median filter to remove noise
    7. Binarise with adaptive threshold (85% of mean, capped at 200)
    """
    from PIL import Image, ImageEnhance, ImageFilter, ImageOps
    import numpy as np

    img = Image.open(image_path)

    # Flatten transparency
    if img.mode in ("RGBA", "P"):
        img = img.convert("RGB")

    # Upscale small images — EasyOCR accuracy drops below ~150dpi
    w, h = img.size
    if max(w, h) < 1200:
        scale = 1200 / max(w, h)
        img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS)

    # Greyscale
    img = img.convert("L")
    arr = np.array(img)

    # Dark card detection — invert so text is always dark on light
    if arr.mean() < 128:
        img = ImageOps.invert(img)

    # Contrast + sharpness
    img = ImageEnhance.Contrast(img).enhance(2.2)
    img = ImageEnhance.Sharpness(img).enhance(2.0)

    # Denoise
    img = img.filter(ImageFilter.MedianFilter(size=3))

    # Adaptive binarise — threshold at 85% of mean brightness, max 200
    arr    = np.array(img)
    thresh = min(int(arr.mean() * 0.85), 200)
    img    = img.point(lambda x: 0 if x < thresh else 255)

    # Save to temp file — same extension as input so EasyOCR reads it correctly
    ext      = os.path.splitext(image_path)[1] or ".png"
    tmp      = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
    tmp_path = tmp.name
    tmp.close()
    img.save(tmp_path)

    return tmp_path


# ── Regex patterns ────────────────────────────────────────────────────────────

_RE_EMAIL = re.compile(
    r'\b[\w.+\-]+@[\w\-]+(?:\.[\w\-]+)*\.[a-zA-Z]{2,}\b'
)

_RE_PHONE = re.compile(
    r'(?<!\w)(\+?[\d][\d\s\(\)\-\.]{6,18}\d)(?!\w)'
)

_RE_WEBSITE = re.compile(
    r'(?:https?://|www\.)\S+|(?<!\@)\b[\w\-]+\.[a-z]{2,}(?:/\S*)?\b',
    re.IGNORECASE,
)

# Lines that are almost certainly not a person's name
_RE_NOT_NAME = re.compile(
    r'''(?xi)
    # Common role/title words
    \b(ceo|cto|coo|cfo|vp|svp|evp|president|director|manager|founder|
       head|lead|senior|principal|engineer|developer|designer|analyst|
       consultant|advisor|partner|associate|officer|executive|chairman|
       co-founder|cofounder|sales|marketing|product|operations|legal|
       finance|account|business|development|strategy|growth|talent|
       human\s+resources?|hr|it|tech|digital|data|software|hardware|
       mobile|cloud|platform|solutions?|services?|group|team|global|
       regional|national|international)\b
    |
    # Company suffixes as standalone line
    \b(ltd|llc|inc|corp|co\.|gmbh|s\.a\.|pty|plc|llp|lp)\b\.?$
    |
    # Pure digits / URLs / emails — already extracted
    ^\s*[\d\s\+\(\)\-\.]+\s*$   # phone-like
    |
    @                            # email fragment
    |
    (?:https?://|www\.)          # URL fragment
    ''',
    re.IGNORECASE
)

# Known role/title keywords for role detection
_ROLE_KEYWORDS = [
    "ceo", "cto", "coo", "cfo", "founder", "co-founder", "cofounder",
    "president", "vice president", "vp", "svp", "evp", "director",
    "manager", "head of", "lead", "principal", "senior", "junior",
    "engineer", "developer", "designer", "architect", "analyst",
    "consultant", "advisor", "partner", "associate", "officer",
    "executive", "chairman", "board", "account", "sales", "marketing",
    "product", "operations", "legal", "finance", "hr", "recruiter",
    "representative", "specialist", "coordinator", "supervisor",
]

# Country / city suffixes that hint at location lines
_RE_LOCATION = re.compile(
    r'''(?xi)
    \b(
      # Countries
      usa|uk|canada|australia|singapore|india|germany|france|
      united\s+states?|united\s+kingdom|new\s+zealand|south\s+africa|
      nigeria|kenya|ghana|egypt|brazil|mexico|colombia|argentina|
      malaysia|indonesia|philippines|thailand|vietnam|china|japan|
      korea|taiwan|hong\s+kong|dubai|uae|israel|
      # State/province abbreviations
      [A-Z]{2}\s+\d{5}|   # US zip: NY 10001
      [A-Z]{1,2}\d{1,2}\s*\d[A-Z]{2}  # UK postcode
    )\b
    |
    ,\s*[A-Z][a-z]+\s*$   # ends with ", City" or ", Country"
    ''',
    re.MULTILINE,
)


# ── Main parser ───────────────────────────────────────────────────────────────

def parse_card_text(text: str) -> dict:
    """
    Parse raw OCR text from a business card into a structured dict.
    Returns keys: full_name, role, company, email, phone, website, location.
    All values are strings or None.
    """
    lines = [l.strip() for l in text.splitlines() if l.strip()]
    claimed: set[int] = set()   # line indices already assigned to a field

    # ── 1. Email — unambiguous ────────────────────────────────────────────
    email = None
    for i, line in enumerate(lines):
        m = _RE_EMAIL.search(line)
        if m:
            email = m.group(0).lower()
            claimed.add(i)
            break

    # ── 2. Phone ──────────────────────────────────────────────────────────
    phone = None
    for i, line in enumerate(lines):
        if i in claimed:
            continue
        m = _RE_PHONE.search(line)
        if m:
            phone = m.group(1).strip()
            claimed.add(i)
            break

    # ── 3. Website ────────────────────────────────────────────────────────
    website = None
    for i, line in enumerate(lines):
        if i in claimed:
            continue
        m = _RE_WEBSITE.search(line)
        if m:
            candidate = m.group(0).lower()
            # Skip if this is actually the email domain
            if email and email.split("@")[-1] in candidate:
                continue
            # Skip very short matches like "co" or "io" alone
            if len(candidate) < 6:
                continue
            website = candidate if candidate.startswith("http") else "https://" + candidate
            claimed.add(i)
            break

    # ── 4. Role ───────────────────────────────────────────────────────────
    role = None
    for i, line in enumerate(lines):
        if i in claimed:
            continue
        ll = line.lower()
        if any(kw in ll for kw in _ROLE_KEYWORDS):
            role = line.strip()
            claimed.add(i)
            break

    # ── 5. Location ───────────────────────────────────────────────────────
    location = None
    for i, line in enumerate(lines):
        if i in claimed:
            continue
        if _RE_LOCATION.search(line):
            location = line.strip()
            claimed.add(i)
            break

    # ── 6. Name + Company from unclaimed lines ────────────────────────────
    # Remaining lines are candidates for name and company.
    remaining = [(i, l) for i, l in enumerate(lines) if i not in claimed]

    name    = None
    company = None

    name_candidates    = []
    company_candidates = []

    for i, line in remaining:
        if _looks_like_name(line):
            name_candidates.append((i, line))
        else:
            company_candidates.append((i, line))

    # Best name candidate: prefer earlier lines (names usually come first)
    if name_candidates:
        name = name_candidates[0][1]

    # Best company: first non-name remaining line
    if company_candidates:
        company = company_candidates[0][1]
    elif len(name_candidates) > 1:
        # Second name-like line is probably the company
        company = name_candidates[1][1]

    return {
        "full_name": name,
        "role":      role,
        "company":   company,
        "email":     email,
        "phone":     phone,
        "website":   website,
        "location":  location,
    }


def _looks_like_name(line: str) -> bool:
    """
    Heuristic: does this line look like a person's name?
    True if 2-4 title-cased words, no role/tech keywords, no digits.
    """
    # Must not contain digits
    if re.search(r'\d', line):
        return False
    # Must not match known non-name patterns
    if _RE_NOT_NAME.search(line):
        return False
    # Split into words
    words = line.split()
    if not (2 <= len(words) <= 4):
        return False
    # Most words should be title-cased (allow ALL-CAPS cards too)
    title_count = sum(1 for w in words if w[0].isupper())
    return title_count >= len(words) - 1


# ── LLM fallback ──────────────────────────────────────────────────────────────

def parse_card_text_with_llm_fallback(text: str) -> dict:
    """
    Run regex parser first. If name is missing, try LLM to fill it in.
    All other fields (email, phone, website, role, location) come from regex —
    the LLM is only asked for the name and company when regex can't find them.
    """
    result = parse_card_text(text)

    # If we got a name from regex, we're done
    if result["full_name"]:
        return result

    # Name missing — ask LLM with a minimal prompt
    try:
        from scrylo.core.llm_adapter import _dispatch, _provider_order, load_cfg
        from scrylo.core.config import load as _load_cfg

        cfg      = _load_cfg()
        groq_key = (cfg.get("llm", {}).get("groq_api_key") or "").strip()

        system = (
            "Extract the person's full name and company name from this business card text. "
            "Reply with ONLY two lines:\nNAME: <full name>\nCOMPANY: <company name>"
        )
        user = f"Business card text:\n{text[:800]}"

        for provider in _provider_order(groq_key, ""):
            r = _dispatch(provider, system, user, groq_key,
                          max_tokens=60, temperature=0.0)
            if r.ok:
                for line in r.text.splitlines():
                    if line.upper().startswith("NAME:"):
                        result["full_name"] = line.split(":", 1)[1].strip()
                        result["_name_via_llm"] = True
                    elif line.upper().startswith("COMPANY:"):
                        result["company"] = line.split(":", 1)[1].strip()
                break
    except Exception:
        pass  # LLM unavailable — return regex result as-is

    return result
