import os
import re
import sys
import json
import time
import email
import imaplib
import smtplib
import tempfile
from typing import Tuple, List, Dict, Optional
from email.header import decode_header, make_header
from email.message import EmailMessage
from email.utils import parseaddr

from dotenv import load_dotenv
from docx import Document
from docx.shared import Cm  # para largura da imagem da assinatura
import language_tool_python

# OpenAI (SDK atual)
try:
    from openai import OpenAI
except Exception:
    OpenAI = None  # fallback se a lib não estiver instalada

# ==============================
# Carregar configuração (.env)
# ==============================

load_dotenv()

IMAP_HOST = os.getenv("IMAP_HOST", "").strip()
IMAP_USER = os.getenv("IMAP_USER", "").strip()
IMAP_PASS = os.getenv("IMAP_PASS", "").strip()
IMAP_LABEL = os.getenv("IMAP_LABEL", "INBOX").strip()

# >>> defaults adaptados para 465/SSL <<<
SMTP_HOST = os.getenv("SMTP_HOST", "").strip()
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
SMTP_USER = os.getenv("SMTP_USER", "").strip()
SMTP_PASS = os.getenv("SMTP_PASS", "").strip()
USE_SMTP_SSL = os.getenv("USE_SMTP_SSL", "true").lower() == "true"

OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "").strip()
DISCIPLINA = os.getenv("DISCIPLINA", "Disciplina").strip()
ASSINATURA = os.getenv("ASSINATURA", "Assinatura").strip()
DESTINO = os.getenv("DESTINO", "").strip()

SIGN_IMAGE_PATH = os.getenv("SIGN_IMAGE_PATH", "").strip()
SIGN_IMAGE_WIDTH_CM = float(os.getenv("SIGN_IMAGE_WIDTH_CM", "5") or 5)

MAX_ISSUES = int(os.getenv("MAX_ISSUES", "0"))  # 0 = nenhum erro tolerado
REPLY_SENDER_ON_ERROR = os.getenv("REPLY_SENDER_ON_ERROR", "true").lower() == "true"

# ==============================
# Utilidades
# ==============================

def log(msg: str):
    print(time.strftime("[%Y-%m-%d %H:%M:%S]"), msg, flush=True)


def clean_subject(raw: Optional[str]) -> str:
    try:
        return str(make_header(decode_header(raw))) if raw else "(sem assunto)"
    except Exception:
        return raw or "(sem assunto)"


def extract_docx_text(bytes_content: bytes) -> str:
    """Extrai o texto de um arquivo .docx (em memória)."""
    with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
        tmp.write(bytes_content)
        tmp.flush()
        path = tmp.name
    try:
        doc = Document(path)
        text = "\n".join(p.text for p in doc.paragraphs)
        return text.strip()
    finally:
        try:
            os.remove(path)
        except Exception:
            pass


def append_signature_to_docx(bytes_content: bytes, assinatura_texto: str) -> bytes:
    """
    Acrescenta assinatura ao final do .docx:
    - Se SIGN_IMAGE_PATH existir, insere a imagem (largura = SIGN_IMAGE_WIDTH_CM).
    - Caso contrário, insere a assinatura em texto (linhas separadas por \n).
    """
    with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
        tmp.write(bytes_content)
        tmp.flush()
        in_path = tmp.name
    out_path = None
    try:
        doc = Document(in_path)
        doc.add_paragraph("")  # espaço antes da assinatura

        if SIGN_IMAGE_PATH and os.path.exists(SIGN_IMAGE_PATH):
            p = doc.add_paragraph()
            run = p.add_run()
            run.add_picture(SIGN_IMAGE_PATH, width=Cm(SIGN_IMAGE_WIDTH_CM))
        else:
            for line in assinatura_texto.splitlines():
                doc.add_paragraph(line)

        out_path = in_path.replace(".docx", "_ASSINADO.docx")
        doc.save(out_path)
        with open(out_path, "rb") as f:
            return f.read()
    finally:
        for pth in [in_path, out_path]:
            if pth and os.path.exists(pth):
                try:
                    os.remove(pth)
                except Exception:
                    pass


def spelling_issues_pt(text: str, lang_code: str = "pt-BR") -> List[Dict]:
    """
    Retorna lista de possíveis problemas de ortografia/gramática detectados.
    Compatível com versões do language_tool_python que retornam str ou objetos nas 'replacements'.
    """
    try:
        tool = language_tool_python.LanguageTool(lang_code)
        matches = tool.check(text or "")
    except Exception as e:
        log(f"AVISO: LanguageTool indisponível: {e}. Ortografia não verificada.")
        return []

    issues = []
    for m in matches:
        start = max(0, getattr(m, "offset", 0))
        length = getattr(m, "errorLength", 0) or 0
        context = (
            (text[start-30:start] if start > 0 else "")
            + "⟦" + text[start:start+length] + "⟧"
            + text[start+length:start+length+30]
        )

        # normaliza replacements para lista de strings
        rep_list = []
        for rep in (getattr(m, "replacements", []) or [])[:3]:
            rep_list.append(getattr(rep, "value", rep if isinstance(rep, str) else str(rep)))

        issues.append({
            "message": getattr(m, "message", ""),
            "context": context,
            "replacements": rep_list,
            "rule": getattr(m, "ruleId", ""),
        })
    return issues


def discipline_ok(text: str, disciplina: str) -> Tuple[bool, str]:
    """
    Usa um LLM (OpenAI) para avaliar se o conteúdo é adequado à disciplina.
    Retorna (ok, motivo).
    """
    if not OPENAI_API_KEY or OpenAI is None:
        # Sem LLM, fallback conservador
        return False, "Validação automática desativada (sem OPENAI_API_KEY ou SDK)."

    client = OpenAI(api_key=OPENAI_API_KEY)

    prompt = f"""
Avalie se as perguntas abaixo pertencem e são adequadas à disciplina "{disciplina}".
Responda APENAS em JSON no formato:
{{"ok": true|false, "motivo": "texto curto com justificativa objetiva"}}

Perguntas / conteúdo (máx. 12000 chars):
{text[:12000]}
"""
    try:
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0
        )
        content = (resp.choices[0].message.content or "").strip()
        ok, motivo = False, ""
        try:
            data = json.loads(content)
            ok = bool(data.get("ok", False))
            motivo = str(data.get("motivo", ""))
        except Exception:
            # Se não veio JSON, reaproveita o texto como justificativa
            motivo = content[:300]
        return ok, motivo or "(sem motivo)"
    except Exception as e:
        return False, f"Falha na chamada do LLM: {e}"


def send_mail_with_attachment(
    to_addr: str,
    subject: str,
    body: str,
    filename: str,
    file_bytes: bytes,
    mimetype="application/vnd.openxmlformats-officedocument.wordprocessingml.document"
):
    """Envia um e-mail com anexo via SMTP (SSL 465 ou STARTTLS 587, conforme USE_SMTP_SSL/SMTP_PORT)."""
    if not SMTP_HOST or not SMTP_USER or not SMTP_PASS:
        raise RuntimeError("SMTP não configurado (verifique SMTP_HOST/USER/PASS).")

    msg = EmailMessage()
    msg["From"] = SMTP_USER
    msg["To"] = to_addr
    msg["Subject"] = subject
    msg.set_content(body)

    maintype, subtype = mimetype.split("/", 1)
    msg.add_attachment(file_bytes, maintype=maintype, subtype=subtype, filename=filename)

    if USE_SMTP_SSL:
        # Para servidores que usam SSL direto (porta 465)
        with smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT) as s:
            s.login(SMTP_USER, SMTP_PASS)
            s.send_message(msg)
    else:
        # Para servidores STARTTLS (porta 587)
        with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as s:
            s.starttls()
            s.login(SMTP_USER, SMTP_PASS)
            s.send_message(msg)


def parse_email_address(addr_header: str) -> Optional[str]:
    """Extrai apenas o endereço de e-mail de um header 'From' (ex.: 'Nome <email@dominio>')."""
    if not addr_header:
        return None
    name, addr = parseaddr(addr_header)
    return addr or None

# ==============================
# IMAP: leitura e processamento
# ==============================

def process_inbox(label: str = None):
    """Lê a caixa do IMAP, processa anexos .docx e envia resultados conforme regras."""
    if not IMAP_HOST or not IMAP_USER or not IMAP_PASS:
        raise RuntimeError("IMAP não configurado (verifique IMAP_HOST/USER/PASS).")
    if not DESTINO:
        raise RuntimeError("DESTINO não configurado. Defina DESTINO no .env.")

    label = label or IMAP_LABEL or "INBOX"

    log(f"Conectando ao IMAP: {IMAP_HOST} | label: {label}")
    imap = imaplib.IMAP4_SSL(IMAP_HOST)
    imap.login(IMAP_USER, IMAP_PASS)
    imap.select(label)

    # Busca emails NÃO LIDOS
    status, data = imap.search(None, '(UNSEEN)')
    if status != "OK":
        log("Falha ao buscar emails UNSEEN.")
        imap.close()
        imap.logout()
        return

    ids = [i for i in data[0].split() if i]
    log(f"E-mails não lidos: {len(ids)}")

    for num in ids:
        try:
            res, raw = imap.fetch(num, '(RFC822)')
            if res != "OK":
                log("Falha ao fazer fetch do e-mail.")
                continue
            msg = email.message_from_bytes(raw[0][1])
            subject = clean_subject(msg.get("Subject"))
            from_header = msg.get("From", "")
            from_addr = parse_email_address(from_header)

            docx_parts: List[bytes] = []
            filenames: List[str] = []

            if msg.is_multipart():
                for part in msg.walk():
                    # >>> robusto contra Header object
                    disposition = (part.get_content_disposition() or "").lower()
                    cdisp_str = str(part.get("Content-Disposition") or "").lower()
                    if disposition == "attachment" or "attachment" in cdisp_str:
                        fname = part.get_filename()
                        if fname:
                            try:
                                fname = str(make_header(decode_header(fname)))
                            except Exception:
                                pass
                        if fname and fname.lower().endswith(".docx"):
                            payload = part.get_payload(decode=True)
                            if payload:
                                docx_parts.append(payload)
                                filenames.append(fname)

            # Caso não haja anexos .docx
            if not docx_parts:
                # Marca como lido e segue
                imap.store(num, '+FLAGS', '\\Seen')
                continue

            # Processar cada .docx
            for i, file_bytes in enumerate(docx_parts):
                fname = filenames[i] if i < len(filenames) else "exame.docx"
                log(f"Processando: {fname} | De: {from_header} | Assunto: {subject}")

                # 1) extrair texto
                text = extract_docx_text(file_bytes)

                # 2) ortografia/gramática
                issues = spelling_issues_pt(text)
                has_too_many_issues = len(issues) > MAX_ISSUES

                if has_too_many_issues and REPLY_SENDER_ON_ERROR and from_addr:
                    # Prepara preview dos primeiros 10 problemas
                    preview = "\n".join([
                        f"- {x['message']} | {x['context']} | Sug.: {', '.join(map(str, x['replacements']))}"
                        for x in issues[:10]
                    ])
                    body = (
                        f"Olá,\n\nDetectei {len(issues)} possíveis problemas de ortografia/gramática no exame \"{fname}\".\n"
                        f"Exemplos:\n{preview}\n\n"
                        f"Limite permitido: {MAX_ISSUES}. Por favor, corrija e reenvie.\n"
                        "Obrigado!"
                    )
                    try:
                        send_mail_with_attachment(
                            to_addr=from_addr,
                            subject=f"Revisão necessária: {subject or fname}",
                            body=body,
                            filename=fname,
                            file_bytes=file_bytes
                        )
                        log("Aviso de revisão enviado ao remetente.")
                    except Exception as e:
                        log(f"Falha ao notificar remetente: {e}")
                    # Marca como lido e pula
                    continue
                elif has_too_many_issues:
                    log(f"Exame reprovado por ortografia (issues={len(issues)} > MAX_ISSUES={MAX_ISSUES}).")
                    continue

                # 3) adequação à disciplina
                ok, motivo = discipline_ok(text, DISCIPLINA)
                if not ok:
                    if REPLY_SENDER_ON_ERROR and from_addr:
                        body = (
                            f"Olá,\n\nO exame \"{fname}\" não parece adequado à disciplina \"{DISCIPLINA}\".\n"
                            f"Motivo: {motivo}\n\n"
                            "Por favor, ajuste e reenvie. Obrigado!"
                        )
                        try:
                            send_mail_with_attachment(
                                to_addr=from_addr,
                                subject=f"Ajustes necessários: {subject or fname}",
                                body=body,
                                filename=fname,
                                file_bytes=file_bytes
                            )
                            log("Aviso de inadequação à disciplina enviado ao remetente.")
                        except Exception as e:
                            log(f"Falha ao notificar remetente: {e}")
                    else:
                        log(f"Reprovado por disciplina: {motivo}")
                    continue

                # 4) Tudo ok: assina e envia ao destino
                signed_bytes = append_signature_to_docx(file_bytes, ASSINATURA)
                out_name = re.sub(r"\.docx$", "", fname, flags=re.IGNORECASE) + "_ASSINADO.docx"
                body = (
                    f"Exame validado para a disciplina \"{DISCIPLINA}\".\n"
                    "Assinatura adicionada conforme solicitado.\n\n"
                    "Atenciosamente,"
                )
                try:
                    send_mail_with_attachment(
                        to_addr=DESTINO,
                        subject=f"Exame validado – {subject or fname}",
                        body=body,
                        filename=out_name,
                        file_bytes=signed_bytes
                    )
                    log(f"Enviado ao destino: {DESTINO} | Arquivo: {out_name}")
                except Exception as e:
                    log(f"Falha ao enviar ao destino: {e}")

            # marca o e-mail como lido ao final
            imap.store(num, '+FLAGS', '\\Seen')

        except Exception as e:
            log(f"Erro ao processar e-mail id {num}: {e}")

    imap.close()
    imap.logout()
    log("Processamento concluído.")

# ==============================
# Execução direta
# ==============================

def main():
    try:
        process_inbox()
    except Exception as e:
        log(f"Erro fatal: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()
