"""Extract text and skills from uploaded CV/resume files."""
import re
from pathlib import Path

from . import ocr
from .matching_engine import normalize_text

COMMON_SKILLS = [
    'python', 'java', 'javascript', 'typescript', 'react', 'angular', 'vue',
    'django', 'flask', 'node.js', 'nodejs', 'sql', 'mysql', 'postgresql',
    'mongodb', 'aws', 'azure', 'docker', 'kubernetes', 'git', 'linux',
    'excel', 'word', 'powerpoint', 'microsoft office', 'project management',
    'communication', 'leadership', 'teamwork', 'problem solving',
    'customer service', 'sales', 'marketing', 'accounting', 'finance',
    'hr', 'human resources', 'data analysis', 'analytics', 'machine learning',
    'ai', 'artificial intelligence', 'html', 'css', 'php', 'laravel', 'spring',
    'c++', 'c#', '.net', 'ruby', 'rails', 'swift', 'kotlin', 'android', 'ios',
    'figma', 'photoshop', 'illustrator', 'seo', 'content writing',
    'negotiation', 'budgeting', 'forecasting', 'audit', 'taxation',
    'supply chain', 'logistics', 'procurement', 'quality assurance',
    'agile', 'scrum', 'jira', 'confluence', 'sap', 'oracle', 'quickbooks',
    'networking', 'cybersecurity', 'cloud computing', 'devops', 'ci/cd',
    'civil engineering', 'electrical engineering', 'mechanical engineering',
    'nursing', 'teaching', 'research', 'public speaking', 'presentation',
    'stakeholder management', 'business development', 'crm', 'erp',
]

SUPPORTED_CV_EXTENSIONS = {'.pdf', '.doc', '.docx', '.txt', '.rtf', '.odt', '.png', '.jpg', '.jpeg', '.webp', '.bmp', '.tif', '.tiff'}


def _read_pdf(file_obj):
    from pypdf import PdfReader

    reader = PdfReader(file_obj)
    if reader.is_encrypted:
        try:
            reader.decrypt('')
        except Exception:
            return ''
    parts = []
    for page in reader.pages:
        try:
            text = page.extract_text()
        except Exception:
            continue
        if text:
            parts.append(text)
    return '\n'.join(parts)


def _read_docx(file_obj):
    from docx import Document

    document = Document(file_obj)
    parts = []
    for paragraph in document.paragraphs:
        if paragraph.text.strip():
            parts.append(paragraph.text)
    for table in document.tables:
        for row in table.rows:
            cells = [cell.text.strip() for cell in row.cells if cell.text.strip()]
            if cells:
                parts.append(' '.join(cells))
    return '\n'.join(parts)


def _read_doc(file_obj):
    """Extract readable text from legacy Word .doc (binary) files."""
    data = file_obj.read()
    chunks = []

    utf16 = data.decode('utf-16le', errors='ignore')
    for match in re.findall(r'[\w\s\.,;:\-\(\)/&@#]{6,}', utf16):
        cleaned = match.strip()
        if cleaned and not cleaned.startswith('Root Entry'):
            chunks.append(cleaned)

    for match in re.findall(rb'[\x20-\x7e\n\r\t]{6,}', data):
        text = match.decode('ascii', errors='ignore').strip()
        if text:
            chunks.append(text)

    seen = set()
    unique = []
    for chunk in chunks:
        key = chunk.lower()
        if key not in seen:
            seen.add(key)
            unique.append(chunk)
    return '\n'.join(unique)


def _read_rtf(raw):
    text = raw.decode('utf-8', errors='ignore') if isinstance(raw, bytes) else raw
    text = re.sub(r'\\[a-z]+\-?\d* ?', ' ', text)
    text = text.replace('\\par', '\n').replace('\\line', '\n')
    text = re.sub(r'[{}]', '', text)
    return re.sub(r'\s+', ' ', text).strip()


def _read_odt(file_obj):
    import zipfile
    from io import BytesIO
    from xml.etree import ElementTree

    data = file_obj.read()
    with zipfile.ZipFile(BytesIO(data)) as archive:
        content = archive.read('content.xml')
    root = ElementTree.fromstring(content)
    namespace = {'text': 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'}
    parts = []
    for node in root.iter('{urn:oasis:names:tc:opendocument:xmlns:text:1.0}p'):
        if node.text:
            parts.append(node.text)
        parts.extend(child.tail for child in node if child.tail)
    return '\n'.join(part.strip() for part in parts if part and part.strip())


def _extract_binary_text(raw):
    """Last-resort text extraction for unknown or mislabeled files."""
    chunks = []
    for match in re.findall(rb'[\x20-\x7e\n\r\t]{8,}', raw):
        text = match.decode('ascii', errors='ignore').strip()
        if text:
            chunks.append(text)

    utf16 = raw.decode('utf-16le', errors='ignore')
    for match in re.findall(r'[\w\s\.,;:\-\(\)/&@#]{8,}', utf16):
        chunks.append(match.strip())

    seen = set()
    unique = []
    for chunk in chunks:
        key = chunk.lower()
        if key not in seen:
            seen.add(key)
            unique.append(chunk)
    return '\n'.join(unique)


def _sniff_kind(raw):
    """Detect the real file type from its first bytes (users often mislabel extensions)."""
    head = raw[:8]
    if head.startswith(b'%PDF'):
        return 'pdf'
    if head.startswith(b'{\\rtf'):
        return 'rtf'
    if head.startswith(b'\x89PNG') or head[:3] == b'\xff\xd8\xff' or head[:2] == b'BM' or head[:4] in (b'II*\x00', b'MM\x00*') or (head[:4] == b'RIFF' and raw[8:12] == b'WEBP'):
        return 'image'
    if head.startswith(b'\xd0\xcf\x11\xe0'):
        return 'doc'
    if head.startswith(b'PK'):
        import zipfile
        from io import BytesIO
        try:
            names = zipfile.ZipFile(BytesIO(raw)).namelist()
        except Exception:
            return ''
        if any(n.startswith('word/') for n in names):
            return 'docx'
        if 'content.xml' in names:
            return 'odt'
    return ''


def extract_text_from_cv(file_field):
    """Return plain text from a Django FileField, or empty string on failure."""
    if not file_field:
        return ''

    name = (getattr(file_field, 'name', '') or '').lower()
    suffix = Path(name).suffix

    try:
        file_field.open('rb')
        try:
            raw = file_field.read()
            if not raw:
                return ''

            from io import BytesIO

            kind = _sniff_kind(raw) or suffix
            if kind in ('.pdf', 'pdf'):
                text = normalize_text(_read_pdf(BytesIO(raw)))
                if ocr.looks_empty(text):
                    scanned = normalize_text(ocr.ocr_bytes(raw, 'pdf'))
                    if len(scanned) > len(text):
                        return scanned
                return text
            if kind == 'image' or kind in ocr.IMAGE_EXTENSIONS:
                return normalize_text(ocr.ocr_bytes(raw, 'image'))
            if kind in ('.docx', 'docx'):
                return normalize_text(_read_docx(BytesIO(raw)))
            if kind in ('.odt', 'odt'):
                return normalize_text(_read_odt(BytesIO(raw)))
            if kind in ('.doc', 'doc'):
                return normalize_text(_read_doc(BytesIO(raw)))
            if kind in ('.rtf', 'rtf'):
                return normalize_text(_read_rtf(raw))
            if kind == '.txt':
                return normalize_text(raw.decode('utf-8', errors='ignore'))

            # Unknown extension: try structured readers, then binary fallback.
            from io import BytesIO
            for reader in (_read_pdf, _read_docx, _read_odt):
                try:
                    text = reader(BytesIO(raw))
                    if text.strip():
                        return text
                except Exception:
                    continue
            return _extract_binary_text(raw)
        finally:
            file_field.close()
    except Exception:
        return ''

    return ''


def extract_skills_from_text(text, job=None, profile_skills=None):
    """Return unique skills found in CV text (dictionary skills + the candidate's own listed skills)."""
    if not text:
        return []
    from .matching_engine import SKILL_DISPLAY, find_skills, normalize_text

    normalized = normalize_text(text)
    lowered = normalized.lower()
    found, seen = [], set()

    def add_skill(raw):
        key = raw.lower().strip()
        if key and len(key) >= 2 and key not in seen:
            seen.add(key)
            found.append(raw.strip())

    for skill in profile_skills or []:
        skill = (skill or '').strip()
        if skill and re.search(r'(?<![A-Za-z0-9+#])' + re.escape(skill.lower()) + r'(?![A-Za-z+#])', lowered):
            add_skill(skill)
    for key in find_skills(normalized):
        add_skill(SKILL_DISPLAY[key])
    return found


def parse_cv(file_field, job=None, profile_skills=None):
    """Parse a CV file and return extracted text and skills."""
    text = extract_text_from_cv(file_field)
    skills = extract_skills_from_text(text, job=job, profile_skills=profile_skills)
    return {'text': text, 'skills': skills}


def parse_text_content(text, job=None, profile_skills=None):
    """Parse plain-text candidate content and return extracted skills."""
    cleaned = (text or '').strip()
    skills = extract_skills_from_text(cleaned, job=job, profile_skills=profile_skills)
    return {'text': cleaned, 'skills': skills}
