import io
import logging
import re
import sys
from datetime import date

from django.core.exceptions import ObjectDoesNotExist
from django.core.files.base import ContentFile
from django.template import Context, Template
from django.utils import timezone
from docx import Document
from docx.shared import Pt

logger = logging.getLogger(__name__)


def _competencies_for_resume(profile):
    from portfolio.competencies_catalog import COMPETENCIES_BY_CODE, VALID_CODES
    from portfolio.models import StudentCompetencies

    try:
        rec = profile.competencies_record
    except ObjectDoesNotExist:
        rec = None
    if not rec:
        return {
            "compet_personal_qualities": "",
            "compet_ok_lines": [],
            "compet_pk_lines": [],
            "compet_additional": "",
            "has_competencies_section": False,
        }
    codes = rec.selected_competency_codes or []
    valid = [c for c in codes if c in VALID_CODES]
    ok_lines = [COMPETENCIES_BY_CODE[c] for c in valid if c.startswith("OK")]
    pk_lines = [COMPETENCIES_BY_CODE[c] for c in valid if c.startswith("PK")]
    pq = (rec.personal_qualities or "").strip()
    extra = (rec.additional_competencies or "").strip()
    has = bool(pq or ok_lines or pk_lines or extra)
    return {
        "compet_personal_qualities": pq,
        "compet_ok_lines": ok_lines,
        "compet_pk_lines": pk_lines,
        "compet_additional": extra,
        "has_competencies_section": has,
    }


def _serialize_profile(profile, request=None):
    photo_url = ""
    if profile.photo:
        url = profile.photo.url
        if request is not None:
            photo_url = request.build_absolute_uri(url)
        else:
            photo_url = url
    parts = [
        profile.last_name or "",
        profile.first_name or "",
        profile.patronymic or "",
    ]
    full_name = " ".join(p for p in parts if p).strip()
    return {
        "first_name": profile.first_name or "",
        "last_name": profile.last_name or "",
        "patronymic": profile.patronymic or "",
        "full_name": full_name,
        "phone": profile.phone or "",
        "email": profile.user.email if profile.user_id else "",
        "bio": profile.bio or "",
        "student_group": profile.student_group or "",
        "specialty": profile.specialty or "",
        "average_grade": str(profile.average_grade) if profile.average_grade is not None else "",
        "photo_url": photo_url,
    }


def build_resume_context(profile, keywords, match_result, request=None):
    """Контекст для шаблонов: ORM-объекты в списках + актуальные ключи для адаптации."""
    kw_skills = [str(k).lower() for k in (keywords.get("skills") or [])]
    kw_tech = [str(k).lower() for k in (keywords.get("technologies") or [])]
    kw_all_set = set(kw_skills) | set(kw_tech)
    soft = list(keywords.get("soft_skills") or [])

    student = _serialize_profile(profile, request=request)

    all_education = list(profile.educations.all().order_by("-start_year"))
    all_experience = list(profile.experiences.all().order_by("-start_date"))
    all_projects = list(profile.projects.all().order_by("-end_date", "-start_date", "-id"))
    all_skills = list(profile.skills.all())
    all_achievements = list(profile.achievements.all().order_by("-date"))

    matched_skill_ids = {s.id for s in match_result["skills"]}
    relevant_skills = [s for s in all_skills if s.id in matched_skill_ids]
    other_skills = [s for s in all_skills if s.id not in matched_skill_ids]
    relevant_skills.sort(key=lambda s: s.name.lower())
    other_skills.sort(key=lambda s: s.name.lower())
    sorted_skills = relevant_skills + other_skills

    matched_projects = list(match_result["projects"])
    matched_pids = {p.id for p in matched_projects}
    projects_with_score = []
    for project in all_projects:
        blob = f"{project.title} {project.description} {project.technologies or ''}".lower()
        score = sum(1 for kw in kw_all_set if kw and kw in blob)
        projects_with_score.append((project, score))
    projects_with_score.sort(key=lambda x: x[1], reverse=True)
    sorted_all_projects = [p[0] for p in projects_with_score]

    order = {p.id: i for i, p in enumerate(matched_projects)}
    projects_highlight = sorted(
        matched_projects, key=lambda p: order.get(p.id, 999)
    )
    projects_other = [p for p in sorted_all_projects if p.id not in matched_pids]

    def ach_score(a):
        blob = f"{a.title} {a.description or ''} {a.organization or ''}".lower()
        return sum(1 for kw in kw_all_set if kw and kw in blob) + sum(
            1 for s in soft if s.lower() in blob
        )

    sorted_achievements = sorted(all_achievements, key=ach_score, reverse=True)

    def edu_score(edu):
        blob = f"{edu.institution} {edu.specialty} {edu.degree or ''}".lower()
        return sum(1 for kw in kw_all_set if kw and kw in blob)

    educations_sorted = sorted(all_education, key=edu_score, reverse=True)

    def exp_score(exp):
        blob = f"{exp.company} {exp.position} {exp.description}".lower()
        return sum(1 for kw in kw_all_set if kw and kw in blob) + sum(
            1 for s in soft if s.lower() in blob
        )

    experiences_sorted = sorted(all_experience, key=exp_score, reverse=True)

    ctx = {
        "student": student,
        "education": educations_sorted,
        "experience": experiences_sorted,
        "projects": sorted_all_projects,
        "projects_highlight": projects_highlight,
        "projects_other": projects_other,
        "skills": sorted_skills,
        "skills_highlight": relevant_skills,
        "skills_other": other_skills,
        "soft_skills": soft,
        "achievements": sorted_achievements,
        "keywords": keywords,
        "generated_date": date.today().isoformat(),
        "generated_at": timezone.now().strftime("%d.%m.%Y"),
        "vacancy_summary": keywords.get("vacancy_summary", ""),
    }
    ctx.update(_competencies_for_resume(profile))
    return ctx


def render_resume_html(template_obj, context_dict):
    css = (template_obj.css_styles or "").strip()
    body_tpl = template_obj.html_template or ""
    looks_full = body_tpl.lstrip().lower().startswith("<!doctype") or bool(
        re.search(r"<html[\s>]", body_tpl[:500], re.I)
    )

    if looks_full:
        html = body_tpl
        if css and "</head>" in html.lower():
            head_part = re.split(r"</head>", html, maxsplit=1, flags=re.I)[0]
            if "<style" not in head_part.lower():
                html = re.sub(
                    r"</head>",
                    f"<style>{css}</style></head>",
                    html,
                    count=1,
                    flags=re.I,
                )
        elif css:
            html = f"<style>{css}</style>" + html
        tpl = Template(html)
        return tpl.render(Context(context_dict))

    full_html = f"""<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>{css}</style>
</head>
<body>
{body_tpl}
</body>
</html>"""
    template = Template(full_html)
    return template.render(Context(context_dict))


def _generate_pdf_weasyprint(html_content, base_url=None):
    """Высокое качество; на Windows часто требует GTK (libgobject)."""
    try:
        from weasyprint import HTML
    except (ImportError, OSError) as e:
        logger.info("WeasyPrint недоступен при импорте: %s", e)
        return None
    try:
        return HTML(string=html_content, base_url=base_url).write_pdf()
    except Exception as e:
        logger.warning("WeasyPrint: ошибка рендера PDF: %s", e)
        return None


def _generate_pdf_xhtml2pdf(html_content):
    """Чистый Python (ReportLab), работает на Windows без GTK; качество проще, чем у WeasyPrint."""
    try:
        from io import BytesIO

        from xhtml2pdf import pisa
    except ImportError:
        return None
    try:
        out = BytesIO()
        src = BytesIO((html_content or "").encode("utf-8"))
        doc = pisa.pisaDocument(src, dest=out, encoding="utf-8")
        if getattr(doc, "err", 0):
            logger.warning("xhtml2pdf: err-флаг при генерации PDF")
            return None
        data = out.getvalue()
        return data if data else None
    except Exception as e:
        logger.warning("xhtml2pdf: %s", e)
        return None


def generate_pdf_fallback(html_content, base_url=None):
    """
    PDF: сначала WeasyPrint, затем xhtml2pdf (fallback без нативных DLL на Windows).
    Возвращает (bytes | None, сообщение_об_ошибке | None).
    """
    pdf = _generate_pdf_weasyprint(html_content, base_url=base_url)
    if pdf:
        return pdf, None

    pdf = _generate_pdf_xhtml2pdf(html_content)
    if pdf:
        logger.info("PDF сформирован резервным движком xhtml2pdf (WeasyPrint недоступен или не справился).")
        return pdf, None

    msg = (
        "PDF не создан: WeasyPrint недоступен (часто отсутствует libgobject / GTK на Windows) "
        "и резервный xhtml2pdf не смог обработать HTML. "
        "См. docs/WINDOWS_PDF_SETUP.md. Доступны экспорт HTML и DOCX."
    )
    if sys.platform.startswith("win"):
        msg += " На Windows можно установить GTK Runtime для WeasyPrint или использовать HTML/DOCX."
    return None, msg


def html_to_pdf_bytes(html_string, base_url=None):
    pdf, _err = generate_pdf_fallback(html_string, base_url=base_url)
    return pdf


def generate_docx_from_html(html_content):
    """Генерирует DOCX из HTML (fallback без оформления WeasyPrint, работает на Windows)."""
    try:
        from bs4 import BeautifulSoup
    except ImportError:
        return None

    try:
        document = Document()
        style = document.styles["Normal"]
        style.font.name = "Calibri"
        style.font.size = Pt(11)

        soup = BeautifulSoup(html_content or "", "html.parser")
        for tag in soup.find_all(["h1", "h2", "h3", "p", "li"]):
            text = tag.get_text(" ", strip=True)
            if not text:
                continue
            name = tag.name.lower()
            if name == "h1":
                document.add_heading(text, level=0)
            elif name == "h2":
                document.add_heading(text, level=1)
            elif name == "h3":
                document.add_heading(text, level=2)
            elif name == "li":
                document.add_paragraph(text, style="List Bullet")
            else:
                document.add_paragraph(text)

        buf = io.BytesIO()
        document.save(buf)
        buf.seek(0)
        return buf.read()
    except Exception:
        return None


def build_docx_from_context(context_dict):
    """DOCX напрямую из структурированного контекста (предпочтительнее HTML-парсинга)."""
    doc = Document()
    style = doc.styles["Normal"]
    style.font.name = "Calibri"
    style.font.size = Pt(11)

    s = context_dict["student"]
    doc.add_heading(s.get("full_name") or "Резюме", 0)
    if s.get("email"):
        doc.add_paragraph(f"Email: {s['email']}")
    if s.get("phone"):
        doc.add_paragraph(f"Телефон: {s['phone']}")
    if s.get("student_group"):
        doc.add_paragraph(f"Группа: {s['student_group']}")
    if s.get("specialty"):
        doc.add_paragraph(f"Специальность: {s['specialty']}")
    if s.get("average_grade"):
        doc.add_paragraph(f"Средний балл: {s['average_grade']}")
    if s.get("bio"):
        doc.add_paragraph(s["bio"])

    if context_dict.get("has_competencies_section"):
        doc.add_heading("Личные качества и компетенции", level=1)
        if context_dict.get("compet_personal_qualities"):
            doc.add_paragraph(
                "Личные качества: " + context_dict["compet_personal_qualities"]
            )
        oks = context_dict.get("compet_ok_lines") or []
        if oks:
            doc.add_heading("Общие компетенции (ОК)", level=2)
            for line in oks:
                doc.add_paragraph(line, style="List Bullet")
        pks = context_dict.get("compet_pk_lines") or []
        if pks:
            doc.add_heading("Профессиональные компетенции (ПК)", level=2)
            for line in pks:
                doc.add_paragraph(line, style="List Bullet")
        if context_dict.get("compet_additional"):
            doc.add_paragraph(
                "Дополнительные компетенции: " + context_dict["compet_additional"]
            )

    def add_skills_block(title, skill_list):
        if not skill_list:
            return
        doc.add_heading(title, level=1)
        for sk in skill_list:
            doc.add_paragraph(f"• {sk.name} — {sk.get_level_display()}")

    add_skills_block("Ключевые навыки (по вакансии)", context_dict.get("skills_highlight"))
    add_skills_block("Прочие навыки", context_dict.get("skills_other"))

    educations = context_dict.get("education") or []
    if educations:
        doc.add_heading("Образование", level=1)
        for e in educations:
            years = f"{e.start_year}—"
            if e.is_current:
                years += "н.в."
            else:
                years += str(e.end_year or "н.в.")
            line = f"{e.institution}, {e.specialty} ({years})"
            if e.degree:
                line += f" — {e.degree}"
            doc.add_paragraph(line)

    experiences = context_dict.get("experience") or []
    if experiences:
        doc.add_heading("Опыт", level=1)
        for x in experiences:
            doc.add_paragraph(f"{x.position}, {x.company}", style="Heading 3")
            end = "н.в." if x.is_current else (str(x.end_date) if x.end_date else "н.в.")
            doc.add_paragraph(f"{x.start_date} — {end}")
            doc.add_paragraph(x.description or "")

    for label, key in (
        ("Проекты (релевантные)", "projects_highlight"),
        ("Другие проекты", "projects_other"),
    ):
        plist = context_dict.get(key) or []
        if not plist:
            continue
        doc.add_heading(label, level=1)
        for p in plist:
            doc.add_paragraph(p.title, style="Heading 3")
            doc.add_paragraph(p.get_project_type_display())
            doc.add_paragraph(p.description or "")
            if p.technologies:
                doc.add_paragraph(f"Технологии: {p.technologies}")

    achievements = context_dict.get("achievements") or []
    if achievements:
        doc.add_heading("Достижения", level=1)
        for a in achievements:
            doc.add_paragraph(
                f"{a.title} — {a.result} ({a.date})"
                + (f", {a.organization}" if a.organization else "")
            )

    buf = io.BytesIO()
    doc.save(buf)
    buf.seek(0)
    return buf.read()


def save_generated_files(instance, html_string, context_dict, base_url=None):
    """Сохраняет HTML, PDF (если WeasyPrint доступен) и DOCX. Возвращает ошибку PDF или None."""
    instance.html_content = html_string or ""

    pdf_bytes, pdf_err = generate_pdf_fallback(html_string, base_url=base_url)
    if pdf_bytes:
        name_pdf = f"resume_{instance.id or 'new'}.pdf"
        instance.pdf_file.save(name_pdf, ContentFile(pdf_bytes), save=False)
    elif pdf_err:
        logger.warning("WeasyPrint / PDF пропущен: %s", pdf_err)

    docx_bytes = None
    try:
        docx_bytes = build_docx_from_context(context_dict)
    except Exception:
        logger.exception("DOCX из контекста не сформирован, пробуем HTML")
        docx_bytes = None
    if not docx_bytes:
        docx_bytes = generate_docx_from_html(html_string)
    if docx_bytes:
        name_docx = f"resume_{instance.id or 'new'}.docx"
        instance.docx_file.save(name_docx, ContentFile(docx_bytes), save=False)

    # Обязательно сохранить запись: иначе html_content и файлы не попадают в БД/хранилище
    instance.save()

    return pdf_err
