"""OCR for scanned / photographed CVs.

Runs fully offline (RapidOCR on ONNX Runtime, models ship inside the pip package)
so there is no external binary to install. Everything is imported lazily so the
site still starts, and normal text CVs still parse, if the OCR packages are absent.
"""
import hashlib
import logging
import threading
from io import BytesIO

from django.core.cache import cache

logger = logging.getLogger(__name__)

MAX_PDF_PAGES = 5
RENDER_DPI = 200
MAX_IMAGE_SIDE = 2600
CACHE_SECONDS = 60 * 60 * 24 * 30
MIN_USEFUL_CHARS = 40

IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tif', '.tiff'}

_engine = None
_engine_failed = False
_lock = threading.Lock()


def _get_engine():
    global _engine, _engine_failed
    if _engine is not None or _engine_failed:
        return _engine
    with _lock:
        if _engine is None and not _engine_failed:
            try:
                from rapidocr_onnxruntime import RapidOCR
                _engine = RapidOCR()
            except Exception:
                logger.warning('OCR engine unavailable', exc_info=True)
                _engine_failed = True
    return _engine


def is_available():
    return _get_engine() is not None


def _lines_to_text(result):
    """Turn RapidOCR boxes into text in reading order (rows top to bottom, left to right)."""
    items = []
    for box, text, *_ in result or []:
        text = (text or '').strip()
        if not text:
            continue
        ys = [p[1] for p in box]
        xs = [p[0] for p in box]
        items.append((min(ys), max(ys) - min(ys), min(xs), text))
    if not items:
        return ''
    items.sort(key=lambda i: i[0])
    rows, current, row_y, row_h = [], [], None, 1
    for top, height, left, text in items:
        if row_y is None or abs(top - row_y) > max(row_h, height) * 0.6:
            if current:
                rows.append(current)
            current, row_y, row_h = [], top, height
        current.append((left, text))
    if current:
        rows.append(current)
    return '\n'.join(' '.join(t for _, t in sorted(row)) for row in rows)


def _prepare_image(data):
    """Decode, fix phone-photo rotation, flatten transparency and cap the size."""
    from PIL import Image, ImageOps

    img = Image.open(BytesIO(data))
    img = ImageOps.exif_transpose(img)
    if img.mode in ('RGBA', 'LA', 'P'):
        img = img.convert('RGBA')
        bg = Image.new('RGB', img.size, 'white')
        bg.paste(img, mask=img.split()[-1])
        img = bg
    else:
        img = img.convert('RGB')
    side = max(img.size)
    if side > MAX_IMAGE_SIDE:
        scale = MAX_IMAGE_SIDE / side
        img = img.resize((int(img.width * scale), int(img.height * scale)))
    return img


def _ocr_pil(img):
    import numpy as np

    engine = _get_engine()
    if engine is None:
        return ''
    result, _ = engine(np.array(img))
    return _lines_to_text(result)


def ocr_image_bytes(data):
    try:
        return _ocr_pil(_prepare_image(data))
    except Exception:
        logger.warning('Image OCR failed', exc_info=True)
        return ''


def ocr_pdf_bytes(data, max_pages=MAX_PDF_PAGES):
    """Render each page of a scanned PDF to an image and read it."""
    try:
        import pymupdf as fitz
    except Exception:
        return ''
    parts = []
    try:
        doc = fitz.open(stream=data, filetype='pdf')
        if doc.needs_pass:
            return ''
        for index in range(min(len(doc), max_pages)):
            pix = doc[index].get_pixmap(dpi=RENDER_DPI)
            page_text = ocr_image_bytes(pix.tobytes('png'))
            if page_text:
                parts.append(page_text)
    except Exception:
        logger.warning('PDF OCR failed', exc_info=True)
    return '\n\n'.join(parts)


def ocr_bytes(data, kind):
    """OCR raw bytes ('pdf' or an image). Results are cached by content hash."""
    if not data or not is_available():
        return ''
    key = 'ats-ocr:' + hashlib.sha1(data).hexdigest()
    cached = cache.get(key)
    if cached is not None:
        return cached
    text = ocr_pdf_bytes(data) if kind == 'pdf' else ocr_image_bytes(data)
    cache.set(key, text, CACHE_SECONDS)
    return text


def looks_empty(text):
    return len((text or '').strip()) < MIN_USEFUL_CHARS
