import os
os.environ["CUDA_VISIBLE_DEVICES"] = ""

import re
import sys
import json
import email
import imaplib
import smtplib
import logging
import mimetypes
import io
import codecs
import subprocess
import tempfile
import unicodedata
from pathlib import Path
from typing import Tuple, List, Dict, Optional
from email.header import decode_header
from email.message import EmailMessage
from email.utils import parseaddr
from email.mime.text import MIMEText

from dotenv import load_dotenv
from docx import Document
from docx.oxml import parse_xml

# ==============================
# Configurações e Logs
# ==============================
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] - %(message)s",
    handlers=[logging.FileHandler("app.log", encoding="utf-8"), logging.StreamHandler(sys.stdout)],
)
load_dotenv()

# --- CARREGAMENTO DE IA E FERRAMENTAS ---
try:
    from openai import OpenAI
except ImportError:
    OpenAI = None

try:
    import language_tool_python
except ImportError:
    language_tool_python = None

try:
    from spellchecker import SpellChecker
except ImportError:
    SpellChecker = None

# --- STOPWORDS (para evitar falsos positivos de formatação) ---
STOPWORDS_PT = {
    "a","o","os","as","um","uma","uns","umas",
    "de","do","da","dos","das","no","na","nos","nas",
    "e","ou","mas","que","se","ao","aos","à","às",
    "por","para","com","sem","em","como","não","sim",
    "era","são","é","foi","ser","estar",
    "vai","vem","ter","uma","um","ao","rio","dia","ano"
}

# --- LISTA BRANCA (TERMOS TÉCNICOS) ---
WHITELIST_TECNICA = {
    "smpp", "osi", "resolution", "protocol", "software", "hardware",
    "mouse", "keyboard", "backend", "frontend", "fullstack", "framework", "api",
    "rest", "json", "xml", "html", "css", "sql", "nosql", "docker", "kubernetes",
    "linux", "windows", "macos", "android", "ios", "ubuntu", "python", "java",
    "javascript", "typescript", "php", "ruby", "c++", "c#", "server", "client",
    "host", "localhost", "ip", "tcp", "udp", "http", "https", "ssh", "ftp", "smtp",
    "imap", "pop3", "dhcp", "dns", "vpn", "lan", "wan", "wlan", "router", "switch",
    "firewall", "proxy", "gateway", "bandwidth", "throughput", "bug", "debug",
    "deploy", "build", "script", "code", "git", "github", "commit", "login", "logout",
    "signin", "signup", "password", "username", "email", "url", "browser", "cookie",
    "cache", "token", "auth", "hash", "algorithm", "array", "string", "int", "float",
    "bool", "class", "object", "void", "null", "nan", "upload", "download", "online",
    "offline", "backup", "database", "data", "pixel", "bit", "byte",
    "kb", "mb", "gb", "tb", "hz", "wifi", "wi-fi", "bluetooth", "iot", "ai", "ml",
    "bot", "crypto", "blockchain", "interface", "ui", "ux", "web", "app", "desktop",
    "mobile", "smart", "icmp", "arp", "rip", "ospf", "bgp", "mpls", "qos", "nat",
    "mac", "vlan", "cpu", "gpu", "ram", "rom", "ssd", "hdd", "usb", "hdmi", "bios",
    "uefi", "driver", "file", "folder", "directory", "root", "admin", "sudo",
    "insutec", "eisi", "ert", "isp", "angola", "luanda", "kz", "ao",
    "1ª", "2ª", "3ª", "4ª", "1º", "2º", "3º", "4º",
}

# ==============================
# Configurações do .env
# ==============================
IMAP_HOST = os.getenv("IMAP_HOST", "").strip().replace('"', "")
IMAP_USER = os.getenv("IMAP_USER", "").strip().replace('"', "")
IMAP_PASS = os.getenv("IMAP_PASS", "").strip().replace('"', "")
IMAP_LABEL = os.getenv("IMAP_LABEL", "INBOX").strip()

SMTP_HOST = os.getenv("SMTP_HOST", "").strip().replace('"', "")
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
SMTP_USER = os.getenv("SMTP_USER", "").strip().replace('"', "")
SMTP_PASS = os.getenv("SMTP_PASS", "").strip().replace('"', "")
USE_SMTP_SSL = os.getenv("USE_SMTP_SSL", "true").lower() == "true"

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini").strip()

DISCIPLINA_FALLBACK = os.getenv("DISCIPLINA", "Não Identificada").strip()
ASSUNTO_PROVA_PALAVRA_CHAVE = os.getenv("ASSUNTO_PROVA_PREFIXO", "Enunciado").strip().replace('"', "")

MAX_ISSUES = int(os.getenv("MAX_ISSUES", "25"))
MAX_FORMAT_ISSUES = int(os.getenv("MAX_FORMAT_ISSUES", "12"))  # sobe um pouco; agora o detector é mais restrito

EMAIL_APROVADO_PARA = os.getenv("EMAIL_APROVADO_PARA", "rouget.fundora@gmail.com").strip()
EMAIL_CC_GERAL = os.getenv("EMAIL_CC_GERAL", "rouget.ruano@insutec.ao").strip()
ASSINATURA = os.getenv("ASSINATURA", "Atenciosamente,\nEquipa de Validação").strip()

SCRIPT_DIR = Path(__file__).resolve().parent
SIGN_IMAGE_PATH_FROM_ENV = os.getenv("SIGN_IMAGE_PATH", "./img/assina.png").strip()
SIGN_IMAGE_PATH_ABSOLUTE = SCRIPT_DIR / SIGN_IMAGE_PATH_FROM_ENV
SIGN_IMAGE_PATH = str(SIGN_IMAGE_PATH_ABSOLUTE)
SIGN_IMAGE_WIDTH_CM = float(os.getenv("SIGN_IMAGE_WIDTH_CM", "5"))

# ==============================
# Inicialização Verificadores
# ==============================
LANG_TOOL_PT = None
try:
    logging.info("A iniciar LanguageTool (PT)...")
    if language_tool_python is None:
        raise RuntimeError("language_tool_python não instalado")
    LANG_TOOL_PT = language_tool_python.LanguageTool("pt-PT")
    logging.info("LanguageTool PT carregado.")
except Exception as e:
    logging.error(f"Falha LanguageTool: {e}")

ENGLISH_CHECKER_FILTER = None
if SpellChecker:
    try:
        ENGLISH_CHECKER_FILTER = SpellChecker(language="en")
    except Exception:
        ENGLISH_CHECKER_FILTER = None

# ==============================
# Funções Utilitárias
# ==============================
def clean_header(raw_header: Optional[str]) -> str:
    if not raw_header:
        return ""
    try:
        decoded_fragments = decode_header(raw_header)
        header_str = ""
        for bytes_content, encoding in decoded_fragments:
            if isinstance(bytes_content, bytes):
                enc = encoding if encoding else "utf-8"
                try:
                    header_str += bytes_content.decode(enc, errors="ignore")
                except LookupError:
                    header_str += bytes_content.decode("cp1252", errors="ignore")
            else:
                header_str += str(bytes_content)
        return header_str.replace("\n", "").replace("\r", "").strip()
    except Exception:
        return str(raw_header)

def verificar_assunto_inteligente(assunto_email: str) -> bool:
    if not assunto_email:
        return False
    texto_normalizado = (
        unicodedata.normalize("NFKD", assunto_email)
        .encode("ASCII", "ignore")
        .decode("utf-8")
        .lower()
    )
    return bool(re.search(r"enunciad[oa]s?", texto_normalizado))

def converter_doc_para_docx(doc_bytes: bytes) -> Optional[bytes]:
    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)
        input_path = temp_dir_path / "temp_input.doc"
        input_path.write_bytes(doc_bytes)
        try:
            cmd = ["libreoffice", "--headless", "--convert-to", "docx", str(input_path), "--outdir", str(temp_dir_path)]
            subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            output_path = temp_dir_path / "temp_input.docx"
            return output_path.read_bytes() if output_path.exists() else None
        except Exception as e:
            logging.error(f"Erro conversão .doc (LibreOffice instalado?): {e}")
            return None

def _header_footer_text(doc: Document) -> List[str]:
    parts: List[str] = []
    for section in doc.sections:
        for hf in (
            section.header,
            section.first_page_header,
            section.even_page_header,
            section.footer,
            section.first_page_footer,
            section.even_page_footer,
        ):
            if hf is None:
                continue
            for p in hf.paragraphs:
                t = (p.text or "").strip()
                if t:
                    parts.append(t)
    return parts

def extract_full_docx_text(bytes_content: bytes) -> str:
    """
    IMPORTANTE: preserva quebras por parágrafo (\\n).
    Isso permite detetar TEXTO I/II e separar excerto vs perguntas.
    """
    try:
        with io.BytesIO(bytes_content) as file_stream:
            doc = Document(file_stream)

            chunks: List[str] = []
            chunks.extend(_header_footer_text(doc))

            chunks.append("--- BODY ---")
            for p in doc.paragraphs:
                t = (p.text or "").strip()
                if t:
                    chunks.append(t)

        # normaliza espaços dentro das linhas, mas preserva \n
        cleaned_lines = [re.sub(r"[ \t]+", " ", line).strip() for line in chunks]
        return "\n".join([ln for ln in cleaned_lines if ln])
    except Exception as e:
        logging.error(f"Erro extração DOCX: {e}")
        return ""

def try_extract_discipline_regex(text: str) -> Optional[str]:
    if not text:
        return None
    header_sample = text[:3000]
    lookahead = r"Ano\s*Lec?tivo\s*:|Data\s*:|Curso\s*:|Docente\s*:|Nome\s*:|Prova\s*:|Duração\s*:|Nº\s*:|Turma\s*:|Ano\s*:|$"
    patterns = [
        r"(?i)(?:Disciplina|Cadeira|Unidade Curricular|Curricular Unit)\s*[:\.]\s*(.+?)\s*(?=" + lookahead + r")",
        r"(?i)(?:Assunto)\s*[:\.]\s*(.+?)\s*(?=" + lookahead + r")",
    ]
    for pattern in patterns:
        m = re.search(pattern, header_sample)
        if m:
            return m.group(1).strip().rstrip(".,;")
    return None

def evaluate_proof_content(text: str, fallback: str) -> Tuple[str, bool, str, str]:
    if not OPENAI_API_KEY or OpenAI is None:
        return fallback, False, "Sem IA", ""

    disciplina = try_extract_discipline_regex(text)
    try:
        client = OpenAI(api_key=OPENAI_API_KEY, timeout=30.0)
    except Exception as e:
        return fallback, False, f"Erro IA (cliente): {e}", ""

    instrucao = f"Disciplina: '{disciplina}'." if disciplina else "Deduza a disciplina."
    prompt = (
        "Aja como Coordenador Pedagógico de Língua Portuguesa. "
        "Faça uma análise antecipada do enunciado. "
        "Verifique clareza, coerência, e risco de 'armadilha' (erros propositais sem aviso explícito). "
        "Se houver excerto literário/cultural, isso é aceitável, mas deve estar devidamente enquadrado. "
        f"{instrucao} "
        'Responda EM JSON: {"disciplina_identificada":"...", "permissivel":true|false, "justificativa":"...", "sugestoes":"..."}'
    )

    try:
        resp = client.chat.completions.create(
            model=OPENAI_MODEL,
            messages=[{"role": "system", "content": prompt}, {"role": "user", "content": text}],
            temperature=0.0,
            response_format={"type": "json_object"},
        )
        data = json.loads(resp.choices[0].message.content or "{}")
        return (
            disciplina or data.get("disciplina_identificada", fallback),
            bool(data.get("permissivel", False)),
            data.get("justificativa", ""),
            data.get("sugestoes", ""),
        )
    except Exception as e:
        return fallback, False, f"Erro IA: {e}", ""

# ==============================
# Anti-armadilha / Contexto
# ==============================
def has_explicit_disclaimer_about_excerpt_or_intentional_errors(text: str) -> bool:
    if not text:
        return False
    t = text.lower()
    sinais = [
        "grafia original",
        "mantém-se a grafia",
        "mantem-se a grafia",
        "excerto",
        "trecho",
        "texto adaptado",
        "ligeiramente adaptado",
        "foi adaptado",
        "erros propositais",
        "erros propositados",
        "deliberadamente incorreto",
        "propositadamente incorreto",
        "para análise linguística",
        "para analise linguistica",
    ]
    return any(s in t for s in sinais)

def detect_excerpt_spans(text: str) -> List[Tuple[int, int]]:
    """
    Agora que o texto tem \\n, a detecção fica robusta:
    - começa em linhas que sejam 'TEXTO I', 'TEXTO 1', 'TEXTO II', etc.
    - termina antes de 'QUESTÕES', 'PERGUNTAS', 'PARTE', 'ITENS', etc. ou antes do próximo TEXTO.
    """
    spans: List[Tuple[int, int]] = []
    if not text:
        return spans

    # Trabalha em offsets reais do texto
    lines = text.splitlines()
    # Guarda (line_start_offset, line_text)
    offsets: List[Tuple[int, str]] = []
    off = 0
    for ln in lines:
        offsets.append((off, ln))
        off += len(ln) + 1  # + '\n'

    texto_hdr = re.compile(r"(?i)^\s*texto\s*(?:i|ii|iii|iv|v|1|2|3|4|5)\b")
    stop_hdr = re.compile(r"(?i)^\s*(quest(?:ões|oes)|perguntas|itens|exerc[íi]cios|parte\s+[ivx]+|parte\s+\d+)\b")

    # localiza linhas de TEXTO
    texto_lines = [i for i, (_, ln) in enumerate(offsets) if texto_hdr.search(ln)]
    if not texto_lines:
        return spans

    for idx, start_i in enumerate(texto_lines):
        start_off = offsets[start_i][0]
        end_i = texto_lines[idx + 1] if idx + 1 < len(texto_lines) else len(offsets)

        # procura stop header entre start_i+1 e end_i
        cut_i = None
        for j in range(start_i + 1, end_i):
            if stop_hdr.search(offsets[j][1]):
                cut_i = j
                break

        if cut_i is not None:
            end_off = offsets[cut_i][0]
        else:
            # até ao início do próximo TEXTO (ou fim)
            end_off = offsets[end_i - 1][0] + len(offsets[end_i - 1][1])

        if end_off - start_off >= 80:
            spans.append((start_off, end_off))

    return spans

def offset_in_any_span(offset: int, spans: List[Tuple[int, int]]) -> bool:
    for a, b in spans:
        if a <= offset < b:
            return True
    return False

def detect_formatting_breaks(text: str) -> List[Dict]:
    """
    Detetor RESTRITO para evitar falsos positivos:
    Só marca padrões típicos de palavra partida tipo:
      'colo ca ndo', 'refle c te'
    E ignora se algum token for stopword (ex.: 'ano não vem', 'vai ao rio').
    """
    issues: List[Dict] = []
    if not text:
        return issues

    # 3 partes: 2-5 + 1-3 + 1-3 (ex.: colo ca ndo, refle c te)
    pat3 = re.compile(r"\b([A-Za-zÀ-ÖØ-öø-ÿ]{2,5})\s+([A-Za-zÀ-ÖØ-öø-ÿ]{1,3})\s+([A-Za-zÀ-ÖØ-öø-ÿ]{1,3})\b")

    for m in pat3.finditer(text):
        a, b, c = m.group(1), m.group(2), m.group(3)
        al, bl, cl = a.lower(), b.lower(), c.lower()

        # ignora se algum for stopword
        if al in STOPWORDS_PT or bl in STOPWORDS_PT or cl in STOPWORDS_PT:
            continue

        combined_len = len(a) + len(b) + len(c)
        # suspeito quando o total forma algo "comprido" (>=7)
        if combined_len < 7:
            continue

        start, end = m.start(), m.end()
        ctx = text[max(0, start - 25):start] + f"[[{text[start:end]}]]" + text[end:end + 25]
        issues.append({"message": "Possível palavra partida por espaços (formatação).", "context": ctx, "rule": "FORMAT_SPLIT3"})

    # dedup
    seen = set()
    uniq = []
    for it in issues:
        if it["context"] in seen:
            continue
        seen.add(it["context"])
        uniq.append(it)

    return uniq[:80]

def is_technical_or_token(word: str) -> bool:
    if not word:
        return True
    w = word.strip()
    wl = w.lower()
    if wl in WHITELIST_TECNICA:
        return True
    if len(w) < 3:
        return True
    if any(ch.isdigit() for ch in w):
        return True
    if re.search(r"[_/\\@#%=]", w):
        return True
    return False

def check_spelling_with_context(tool_pt, text: str, allow_english: bool, excerpt_spans: List[Tuple[int, int]]) -> Dict[str, List[Dict]]:
    out = {
        "teacher_errors_counted": [],
        "teacher_unknown": [],
        "excerpt_errors": [],
        "excerpt_unknown": [],
        "ignored": [],
    }
    if tool_pt is None:
        return out

    try:
        matches = tool_pt.check(text or "")
        ALLOWED_RULES = {"MORFOLOGIK_RULE_PT_PT"}

        for m in matches:
            if m.ruleId not in ALLOWED_RULES:
                continue

            raw = text[m.offset:m.offset + m.errorLength]
            w = raw.strip(".,;:?!()[]{}\"'")
            if not w:
                continue

            ctx = (
                text[max(0, m.offset - 25):m.offset]
                + f"[[{text[m.offset:m.offset+m.errorLength]}]]"
                + text[m.offset + m.errorLength:m.offset + m.errorLength + 25]
            )

            # técnicos/tokens
            if is_technical_or_token(w):
                out["ignored"].append({"message": "Ignorado (técnico/token).", "context": ctx, "rule": m.ruleId})
                continue

            # nomes próprios bem formados
            if w[0].isupper():
                out["ignored"].append({"message": "Ignorado (nome próprio com Maiúscula).", "context": ctx, "rule": m.ruleId})
                continue

            # inglês
            if allow_english and ENGLISH_CHECKER_FILTER:
                try:
                    if not ENGLISH_CHECKER_FILTER.unknown([w.lower()]):
                        out["ignored"].append({"message": "Ignorado (inglês).", "context": ctx, "rule": m.ruleId})
                        continue
                except Exception:
                    pass

            in_excerpt = offset_in_any_span(m.offset, excerpt_spans)
            has_suggestions = bool(getattr(m, "replacements", None))

            # OBS: Se estiver no excerto, nunca “conta” como erro do docente.
            if in_excerpt:
                if has_suggestions:
                    out["excerpt_errors"].append({"message": "Possível erro (no excerto).", "context": ctx, "rule": m.ruleId, "suggestions": list(m.replacements)[:5]})
                else:
                    out["excerpt_unknown"].append({"message": "Fora do dicionário (no excerto).", "context": ctx, "rule": m.ruleId})
            else:
                if has_suggestions:
                    out["teacher_errors_counted"].append({"message": "Possível erro (docente/instruções).", "context": ctx, "rule": m.ruleId, "suggestions": list(m.replacements)[:5]})
                else:
                    out["teacher_unknown"].append({"message": "Fora do dicionário (docente/instruções).", "context": ctx, "rule": m.ruleId})

        return out
    except Exception as e:
        logging.error(f"Erro check_spelling_with_context: {e}")
        return out

# ==============================
# Assinatura + Email
# ==============================
def add_signature_to_doc(doc_bytes: bytes, image_path_str: str, image_width_cm: float) -> Optional[bytes]:
    image_path = Path(image_path_str)
    if not image_path.is_file():
        return doc_bytes
    try:
        doc_stream = io.BytesIO(doc_bytes)
        doc = Document(doc_stream)

        def insert_floating_image_in_header(paragraph, image_path_str, width_cm):
            run = paragraph.add_run()
            rId, image = paragraph.part.get_or_add_image(image_path_str)
            width_emu = int(width_cm * 360000)
            img_size = image.px_width, image.px_height
            height_emu = int(width_emu * img_size[1] / img_size[0])
            graphic_xml = f"""<w:drawing xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"><wp:anchor distT="0" distB="0" distL="114300" distR="114300" simplePos="0" relativeHeight="251658240" behindDoc="1" locked="0" layoutInCell="1" allowOverlap="1"><wp:simplePos x="0" y="0"/><wp:positionH relativeFrom="margin"><wp:align>right</wp:align></wp:positionH><wp:positionV relativeFrom="page"><wp:posOffset>540000</wp:posOffset></wp:positionV><wp:extent cx="{width_emu}" cy="{height_emu}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:wrapNone/><wp:docPr id="1" name="Signature"/><wp:cNvGraphicFramePr><a:graphicFrameLocks noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"><pic:pic><pic:nvPicPr><pic:cNvPr id="0" name="Sig"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="{rId}" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="{width_emu}" cy="{height_emu}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:anchor></w:drawing>"""
            run._r.append(parse_xml(graphic_xml))

        section = doc.sections[0]
        header = section.header
        p = header.paragraphs[0] if header.paragraphs else header.add_paragraph()
        insert_floating_image_in_header(p, str(image_path), image_width_cm)
        out = io.BytesIO()
        doc.save(out)
        return out.getvalue()
    except Exception as e:
        logging.error(f"Erro assinatura DOCX: {e}")
        return None

def send_email(to, sub, body, atts=None, cc=None, assinatura="", sign_image_path="", sign_image_width_cm=5.0):
    if not all([SMTP_HOST, SMTP_USER, SMTP_PASS]):
        return

    msg = EmailMessage()
    msg["From"], msg["To"], msg["Subject"] = SMTP_USER, to, sub
    if cc:
        msg["Cc"] = cc

    html_sig = f"<br><br><p>{assinatura.replace(chr(10), '<br>')}</p>" if assinatura else ""
    msg.set_content(f"{body}\n\n{assinatura}", "plain", "utf-8")
    msg.add_alternative(f"<html><body><p>{body.replace(chr(10), '<br>')}</p>{html_sig}</body></html>", "html", "utf-8")

    if atts:
        msg.make_mixed()
        for fname, fbytes in atts:
            try:
                mt, st = (mimetypes.guess_type(fname)[0] or "application/octet-stream").split("/", 1)
                if fname.endswith(".txt"):
                    part = MIMEText(fbytes.decode("utf-8-sig", errors="replace"), "plain", "utf-8")
                    part.add_header("Content-Disposition", "attachment", filename=fname)
                    msg.attach(part)
                else:
                    msg.add_attachment(fbytes, maintype=mt, subtype=st, filename=fname)
            except Exception:
                pass

    with (smtplib.SMTP_SSL if USE_SMTP_SSL else smtplib.SMTP)(SMTP_HOST, SMTP_PORT) as s:
        if not USE_SMTP_SSL:
            s.starttls()
        s.login(SMTP_USER, SMTP_PASS)
        s.send_message(msg)

# ==============================
# Inbox
# ==============================
def process_inbox():
    if not all([IMAP_HOST, IMAP_USER, IMAP_PASS]):
        logging.error("Configurações de IMAP incompletas.")
        return

    try:
        imap = imaplib.IMAP4_SSL(IMAP_HOST)
        imap.login(IMAP_USER, IMAP_PASS)
        imap.select(f'"{IMAP_LABEL}"')
    except Exception as e:
        logging.error(f"Erro ao conectar IMAP: {e}")
        return

    logging.info("A procurar emails NÃO LIDOS (UNSEEN)...")
    status, data = imap.search(None, "(UNSEEN)")
    if status != "OK" or not data[0]:
        logging.info("Nenhum email novo.")
        imap.close()
        imap.logout()
        return

    for num in data[0].split():
        try:
            res, raw = imap.fetch(num, "(BODY.PEEK[])")
            if res != "OK":
                continue

            msg = email.message_from_bytes(raw[0][1])
            subject = clean_header(msg.get("Subject", ""))

            if not verificar_assunto_inteligente(subject):
                continue

            sender = parseaddr(clean_header(msg.get("From", "")))[1]
            logging.info(f"Processando email de: {sender} | Assunto: {subject}")

            docs_parts = []
            for part in msg.walk():
                raw_filename = part.get_filename()
                if raw_filename:
                    fname = clean_header(raw_filename)
                    is_docx = fname.lower().endswith(".docx")
                    is_old_doc = fname.lower().endswith(".doc")
                    is_temp = fname.startswith("~$")
                    if (is_docx or is_old_doc) and not is_temp:
                        payload = part.get_payload(decode=True)
                        if payload:
                            docs_parts.append((fname, payload))

            if not docs_parts:
                imap.store(num, "+FLAGS", "\\Seen")
                continue

            for fname, fbytes in docs_parts:
                if fname.lower().endswith(".doc"):
                    logging.info(f"Convertendo '{fname}' para .docx...")
                    converted = converter_doc_para_docx(fbytes)
                    if converted:
                        fbytes = converted
                        fname = fname + "x"
                    else:
                        send_email(sender, f"[ERRO] {fname}", "Formato .doc inválido ou corrompido.")
                        continue

                text = extract_full_docx_text(fbytes)
                if not text:
                    continue

                disc_pre = try_extract_discipline_regex(text)
                is_english = bool(disc_pre and any(k in disc_pre.lower() for k in ["inglês", "english"]))

                excerpt_spans = detect_excerpt_spans(text)

                spell = check_spelling_with_context(LANG_TOOL_PT, text, allow_english=is_english, excerpt_spans=excerpt_spans)
                teacher_errors = spell["teacher_errors_counted"]
                teacher_unknown = spell["teacher_unknown"]
                excerpt_errors = spell["excerpt_errors"]
                excerpt_unknown = spell["excerpt_unknown"]
                ignored = spell["ignored"]

                format_issues = detect_formatting_breaks(text)

                has_disclaimer = has_explicit_disclaimer_about_excerpt_or_intentional_errors(text)
                pedagogical_alerts: List[str] = []
                if (excerpt_errors or excerpt_unknown) and not has_disclaimer:
                    pedagogical_alerts.append(
                        "ALERTA PEDAGÓGICO: Há ocorrências no excerto marcadas pelo corretor, mas não existe nota explícita "
                        "do tipo 'mantém-se a grafia original do excerto' ou 'há desvios propositais para análise'."
                    )

                disc, ok_ia, just, sug = evaluate_proof_content(text, DISCIPLINA_FALLBACK)

                counted = len(teacher_errors)
                fmt_count = len(format_issues)

                reprovacao_por_formato = fmt_count > MAX_FORMAT_ISSUES
                reprovacao_por_ortografia_docente = counted > MAX_ISSUES

                aprovado = ok_ia and (not reprovacao_por_formato) and (not reprovacao_por_ortografia_docente)

                status_tag = "APROVADO"
                if aprovado and pedagogical_alerts:
                    status_tag = "APROVADO COM ALERTA"
                elif not aprovado:
                    status_tag = "REVISÃO"

                def fmt_list(title: str, items: List[Dict], limit: int = 25) -> str:
                    if not items:
                        return f"{title}: 0\n"
                    s = f"{title}: {len(items)}\n"
                    for i, it in enumerate(items[:limit], start=1):
                        sug_txt = ""
                        if "suggestions" in it and it["suggestions"]:
                            sug_txt = " | Sugestões: " + ", ".join(it["suggestions"])
                        s += f"{i}. {it.get('context','')}{sug_txt}\n"
                    if len(items) > limit:
                        s += f"... ({len(items) - limit} ocultos)\n"
                    return s

                report = ""
                report += f"VALIDAÇÃO: {status_tag}\n"
                report += f"Disciplina: {disc}\n\n"
                report += f"[IA - Justificativa]: {just}\n\n"
                if sug:
                    report += f"[IA - Sugestões]:\n{sug}\n\n"

                report += "====================\n"
                report += "ANÁLISE ORTOGRÁFICA (com contexto)\n"
                report += "- Conta apenas erros com sugestão fora do excerto.\n"
                report += "- No excerto, serve para alerta (não reprova).\n\n"

                report += fmt_list("Erros do DOCENTE que CONTAM (fora do excerto, com sugestão)", teacher_errors, limit=40)
                report += f"TOTAL (CONTAM) = {counted} | MAX_ISSUES = {MAX_ISSUES}\n\n"
                report += fmt_list("Fora do dicionário (DOCENTE, sem sugestão)", teacher_unknown, limit=25)
                report += "\n"
                report += fmt_list("No excerto: erros com sugestão (NÃO contam)", excerpt_errors, limit=25)
                report += "\n"
                report += fmt_list("No excerto: fora do dicionário (NÃO conta)", excerpt_unknown, limit=25)
                report += "\n"
                report += fmt_list("Ignorados (nomes próprios com Maiúscula / técnicos / inglês)", ignored, limit=25)

                report += "\n====================\n"
                report += "FORMATAÇÃO / QUALIDADE\n"
                report += fmt_list("Possíveis palavras partidas por espaços (realmente suspeitas)", format_issues, limit=30)
                report += f"TOTAL (FORMATAÇÃO) = {fmt_count} | MAX_FORMAT_ISSUES = {MAX_FORMAT_ISSUES}\n"

                if pedagogical_alerts:
                    report += "\n====================\n"
                    report += "ALERTAS PEDAGÓGICOS (anti-armadilha)\n"
                    for a in pedagogical_alerts:
                        report += f"- {a}\n"
                    report += "\nSugestão de nota:\n"
                    report += "  'Mantém-se a grafia original do excerto. Eventuais desvios serão objeto de análise linguística.'\n"

                att_rep = ("relatorio.txt", codecs.BOM_UTF8 + report.encode("utf-8"))
                email_args = {"assinatura": ASSINATURA, "sign_image_path": SIGN_IMAGE_PATH}

                if aprovado:
                    doc_assinado = add_signature_to_doc(fbytes, SIGN_IMAGE_PATH, SIGN_IMAGE_WIDTH_CM)
                    if doc_assinado:
                        final_name = f"ASSINADO_{fname}"
                        send_email(
                            EMAIL_APROVADO_PARA,
                            f"[{status_tag}] {disc}",
                            "Segue relatório e enunciado assinado.",
                            [(final_name, doc_assinado), att_rep],
                            cc=EMAIL_CC_GERAL,
                            **email_args,
                        )
                        send_email(
                            sender,
                            f"[{status_tag}] {disc}",
                            "Aprovado. Segue relatório.",
                            [att_rep, (fname, fbytes)],
                            cc=EMAIL_CC_GERAL,
                            **email_args,
                        )
                    else:
                        send_email(sender, f"[ERRO] {disc}", "Erro ao assinar. Segue relatório.", [att_rep, (fname, fbytes)], cc=EMAIL_CC_GERAL, **email_args)
                else:
                    motivos = []
                    if reprovacao_por_formato:
                        motivos.append("formatação (palavras realmente partidas)")
                    if reprovacao_por_ortografia_docente:
                        motivos.append("erros com sugestão nas instruções/perguntas (fora do excerto)")
                    if not ok_ia:
                        motivos.append("parecer IA (clareza/estrutura)")

                    body = "Necessária revisão.\n"
                    if motivos:
                        body += "Motivos: " + "; ".join(motivos) + ".\n"
                    if pedagogical_alerts:
                        body += "\nAlerta pedagógico: acrescentar nota sobre grafia original/possíveis desvios do excerto.\n"

                    send_email(
                        sender,
                        f"[REVISÃO] {disc}",
                        body,
                        [att_rep, (fname, fbytes)],
                        cc=EMAIL_CC_GERAL,
                        **email_args,
                    )

            imap.store(num, "+FLAGS", "\\Seen")
            logging.info("Email processado com sucesso.")

        except Exception as e:
            logging.error(f"Erro crítico ID {num}: {e}")

    imap.close()
    imap.logout()

def main():
    try:
        process_inbox()
    except Exception as e:
        logging.critical(f"Fatal: {e}")

if __name__ == "__main__":
    main()

