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_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('"', "")

# Tolerâncias
MAX_ISSUES = int(os.getenv("MAX_ISSUES", "12"))            # erros do DOCENTE com sugestão (fora do excerto)
MAX_FORMAT_ISSUES = int(os.getenv("MAX_FORMAT_ISSUES", "12"))

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

# ==============================
# Helpers
# ==============================
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 extract_full_docx_text(bytes_content: bytes) -> str:
    """
    Preserva quebras de parágrafo (\\n) para conseguir detectar blocos.
    """
    try:
        with io.BytesIO(bytes_content) as file_stream:
            doc = Document(file_stream)

            lines: List[str] = []
            # corpo
            lines.append("--- BODY ---")
            for p in doc.paragraphs:
                t = (p.text or "").strip()
                if t:
                    lines.append(re.sub(r"[ \t]+", " ", t))

        return "\n".join(lines)
    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 / Excerto
# ==============================
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",
        "texto adaptado",
        "ligeiramente adaptado",
        "foi adaptado",
        "erros propositais",
        "erros propositados",
        "para análise linguística",
        "para analise linguistica",
    ]
    return any(s in t for s in sinais)

def _line_offsets(text: str) -> List[Tuple[int, str]]:
    lines = text.splitlines()
    offsets: List[Tuple[int, str]] = []
    off = 0
    for ln in lines:
        offsets.append((off, ln))
        off += len(ln) + 1
    return offsets

def detect_excerpt_spans(text: str) -> List[Tuple[int, int]]:
    """
    DETECÇÃO ROBUSTA:
    1) Padrões 'TEXTO I/II/1/2'
    2) OU referência bibliográfica do excerto:
       - linhas que começam por 'In ...' / 'Em ...' / 'Fonte:' / 'Adaptado de'
       - ou contêm '(ligeiramente adaptado)' / 'adaptado'
    Regra: marca como excerto o bloco ANTES da referência bibliográfica até ao início do bloco (ou até perguntas).
    """
    spans: List[Tuple[int, int]] = []
    if not text:
        return spans

    offsets = _line_offsets(text)

    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")

    biblio_hdr = re.compile(
        r"(?i)^\s*(in\s+|em\s+|fonte\s*:|adaptado\s+de\b|extra[ií]do\s+de\b)"
    )
    biblio_inline = re.compile(r"(?i)\(.*adaptad[oa].*\)")

    texto_lines = [i for i, (_, ln) in enumerate(offsets) if texto_hdr.search(ln)]
    biblio_lines = [i for i, (_, ln) in enumerate(offsets) if biblio_hdr.search(ln) or biblio_inline.search(ln)]

    # Caso A: TEXTO I/II etc.
    if texto_lines:
        for idx, start_i in enumerate(texto_lines):
            start_off = offsets[start_i][0]
            next_i = texto_lines[idx + 1] if idx + 1 < len(texto_lines) else len(offsets)

            cut_i = None
            for j in range(start_i + 1, next_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é antes do próximo TEXTO ou fim
                last_i = next_i - 1
                end_off = offsets[last_i][0] + len(offsets[last_i][1])

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

    # Caso B: referência bibliográfica “In ... Pepetela (adaptado)”
    # Marca bloco anterior como excerto até ao início da linha biblio.
    for bi in biblio_lines:
        end_off = offsets[bi][0]

        # procura início do bloco subindo até achar linha vazia lógica/stop/perguntas
        start_i = bi - 1
        while start_i > 0:
            ln = offsets[start_i][1].strip()
            if not ln:
                break
            if stop_hdr.search(ln):
                break
            # se encontrou TEXTO header, começa a partir dele
            if texto_hdr.search(ln):
                break
            start_i -= 1

        # avança um se parou numa linha vazia/stop header
        if start_i < bi and offsets[start_i][1].strip() == "":
            start_i += 1

        start_off = offsets[start_i][0]

        # filtra spans muito pequenos
        if end_off - start_off >= 120:
            spans.append((start_off, end_off))

    # normaliza spans (merge sobrepostos)
    spans.sort()
    merged: List[Tuple[int, int]] = []
    for a, b in spans:
        if not merged:
            merged.append((a, b))
            continue
        la, lb = merged[-1]
        if a <= lb:
            merged[-1] = (la, max(lb, b))
        else:
            merged.append((a, b))

    return merged

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]:
    issues: List[Dict] = []
    if not text:
        return issues

    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()

        if al in STOPWORDS_PT or bl in STOPWORDS_PT or cl in STOPWORDS_PT:
            continue

        if (len(a) + len(b) + len(c)) < 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"})

    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]
            )

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

            # Nome próprio com Maiúscula: não conta
            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))

            if in_excerpt:
                if has_suggestions:
                    out["excerpt_errors"].append({"message": "Possível ocorrência (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"):
                    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: Existem ocorrências marcadas no excerto, mas falta uma nota explícita "
                        "sobre grafia original/adaptação/possíveis desvios para análise linguística."
                    )

                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: ocorrências com sugestão (NÃO contam)", excerpt_errors, limit=40)
                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 (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"))
                e
mail_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")
                    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/adaptação 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()

