import os
import re
import sys
import json
import time
import email
import imaplib
import smtplib
import tempfile
import logging
import mimetypes
import io
import subprocess
import shutil
from pathlib import Path
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, make_msgid
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

from dotenv import load_dotenv
from docx import Document
from docx.shared import Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.text import WD_TAB_ALIGNMENT

try:
    from openai import OpenAI, APIError
except ImportError:
    OpenAI = None
    APIError = None

try:
    import language_tool_python
except ImportError:
    language_tool_python = None

# ==============================
# Constantes e Configurações
# ==============================
PALAVRAS_PERSONALIZADAS = [
    'multicomputador', 'multiprocessador', 'Flynn', 'telemóvel', 'hardware',
    'software', 'backend', 'frontend', 'framework', 'INSUTEC', 'EISI',
    'Arquitetura', 'Computadores', 'SQL', 'script'
]

LISTA_PALAVRAS_INAPROPRIADAS = [
    'merda', 'porra', 'caralho', 'foder', 'puta', 'cona',
    'grosseira', 'estúpido', 'idiota'
]

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()
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()
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()
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", "Enunciados").strip()
MAX_ISSUES = int(os.getenv("MAX_ISSUES", "15"))

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

# --- CORREÇÃO DE CAMINHO ABSOLUTO (PARA ASSINATURA) ---
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"))
REPLY_SENDER_ON_ERROR_FLAG = os.getenv("REPLY_SENDER_ON_ERROR", "true").lower() == "true"


# ==============================
# Funções Utilitárias
# ==============================
def clean_header(raw_header: Optional[str]) -> str:
    if not raw_header: return ""
    try: return str(make_header(decode_header(raw_header)))
    except Exception: return raw_header

def extract_full_docx_text(bytes_content: bytes) -> str:
    full_text = []
    with io.BytesIO(bytes_content) as file_stream:
        doc = Document(file_stream)
        for section in doc.sections:
            for header in (section.header, section.first_page_header, section.even_page_header,
                           section.footer, section.first_page_footer, section.even_page_footer):
                if header is None: continue
                for table in header.tables:
                    for row in table.rows:
                        for cell in row.cells:
                            full_text.append(cell.text)
                for paragraph in header.paragraphs:
                    full_text.append(paragraph.text)

        full_text.append("\n--- CONTEÚDO PRINCIPAL DO DOCUMENTO ---\n")

        for paragraph in doc.paragraphs:
            full_text.append(paragraph.text)
        for table in doc.tables:
            for row in table.rows:
                for cell in row.cells:
                    full_text.append(cell.text)

    return "\n".join(full_text).strip()

def spelling_issues_pt(text: str, custom_words: List[str]) -> List[Dict]:
    if language_tool_python is None: return []
    try:
        tool = language_tool_python.LanguageTool('pt-PT')

        if hasattr(tool, 'disabled_words'): tool.disabled_words.update(custom_words)
        elif hasattr(tool, 'disable_spellchecking_for_words'): tool.disable_spellchecking_for_words(custom_words)

        matches = tool.check(text or "")
        issues = []
        for m in matches:
            context = (text[max(0, m.offset - 30):m.offset] + f"⟦{text[m.offset:m.offset+m.errorLength]}⟧" + text[m.offset+m.errorLength:m.offset+m.errorLength+30])
            issues.append({"message": m.message, "context": context})
        return issues
    except Exception as e:
        logging.warning(f"LanguageTool indisponível: {e}."); return []

def check_for_profanity(text: str, word_list: List[str]) -> List[str]:
    text_lower = text.lower()
    found_words = set()
    for word in word_list:
        try:
            pattern = r'\b' + re.escape(word.lower()) + r'\b'
            if re.search(pattern, text_lower):
                found_words.add(word)
        except re.error as e:
            logging.warning(f"Erro de regex ao procurar por '{word}': {e}")

    return list(found_words)

# ==============================
# NOVA FUNÇÃO: Extração Regex
# ==============================
def try_extract_discipline_regex(text: str) -> Optional[str]:
    """
    Tenta extrair a disciplina explicitamente usando Regex no início do texto (cabeçalho).
    Procura por: 'Disciplina: X', 'Cadeira: Y', 'Unidade Curricular: Z'.
    """
    if not text:
        return None
    
    # Limita a busca aos primeiros 2000 caracteres (geralmente onde está o cabeçalho)
    header_sample = text[:2000]
    
    # Padrões comuns (insensível a maiúsculas/minúsculas devido ao (?i))
    patterns = [
        r"(?i)(?:Disciplina|Cadeira|Unidade Curricular|Curricular Unit)\s*[:\.]\s*([^\n\r]+)",
        r"(?i)(?:Assunto)\s*[:\.]\s*([^\n\r]+)"
    ]
    
    for pattern in patterns:
        match = re.search(pattern, header_sample)
        if match:
            # Retorna o grupo capturado, limpo de espaços e pontuação final
            clean_name = match.group(1).strip().rstrip(".,;")
            logging.info(f"Disciplina detectada via REGEX: '{clean_name}'")
            return clean_name
            
    return None

# ====================================================================
# FUNÇÃO (evaluate_proof_content) - ATUALIZADA COM REGEX
# ====================================================================
def evaluate_proof_content(text: str, fallback: str) -> Tuple[str, bool, str, str]:
    if not OPENAI_API_KEY:
        return fallback, False, "Validação OpenAI desativada (API_KEY ausente ou inválida no ficheiro .env).", ""
    
    # 1. TENTATIVA DE EXTRAÇÃO DETERMINÍSTICA (REGEX)
    disciplina_detectada = try_extract_discipline_regex(text)
    
    client = OpenAI(api_key=OPENAI_API_KEY)

    # Define o prompt com base no sucesso do Regex
    if disciplina_detectada:
        instrucao_disciplina = f"A disciplina foi identificada no cabeçalho via REGEX como: '{disciplina_detectada}'. CONSIDERE ISTO COMO VERDADE ABSOLUTA."
        tarefa_1_desc = f"1. **Identificar a Disciplina:** O sistema já identificou que a disciplina é '{disciplina_detectada}'. Apenas confirme."
        tarefa_3_desc = f"3. **Análise de Relevância (TAREFA 3 - CRÍTICA):** Analise se as perguntas são pertinentes para a disciplina '{disciplina_detectada}'. Se encontrar perguntas de uma matéria COMPLETAMENTE DIFERENTE, 'permissivel' deve ser false."
    else:
        instrucao_disciplina = "A disciplina não está explícita no formato padrão. Tente deduzi-la pelo cabeçalho."
        tarefa_1_desc = "1. **Identificar a Disciplina (TAREFA 1):** Leia o texto e encontre o nome EXATO da disciplina no cabeçalho."
        tarefa_3_desc = "3. **Análise de Relevância (TAREFA 3 - CRÍTICA):** Analise CADA PERGUNTA. Verifique se as competências testadas são relevantes para a disciplina identificada."

    prompt = f"""
    Aja como um Coordenador de Curso e especialista em pedagogia universitária em Angola.
    Analise o seguinte texto extraído de um documento Word (cabeçalho e corpo principal).

    **Contexto Linguístico:** O documento está em Português de Angola.

    {instrucao_disciplina}

    Siga estas tarefas por ordem de PRIORIDADE:

    {tarefa_1_desc}

    2.  **Análise de Profanidade (TAREFA 2 - CRÍTICA):** Verifique se o texto contém linguagem grosseira, imprópria ou ofensiva.
        * Se encontrar, "permissivel" DEVE ser `false`.

    {tarefa_3_desc}

    ---
    **Critérios Pedagógicos (Afetam 'sugestoes')**
    ---
    4.  **Clareza e Nível Cognitivo:** Verifique se as perguntas são claras e usam verbos de ação adequados.
    5.  **Gestão de Contexto:** Se houver erros de copiar/colar no cabeçalho, mas as perguntas forem tecnicamente válidas para a disciplina, aceite como permissível, mas note nas sugestões.

    6.  **Decisão Final (TAREFA 8):**
        * "permissivel": `true` (APENAS se TAREFA 2 e 3 passarem).
        * "permissivel": `false` (Se a TAREFA 2 ou 3 falharem).
    
    Responda APENAS em JSON: {{"disciplina_identificada": "O nome EXATO", "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 "{}")
        
        # Prioridade ao nome detetado pelo REGEX, se existir
        final_disciplina = disciplina_detectada if disciplina_detectada else data.get("disciplina_identificada", fallback)

        return (
            final_disciplina,
            data.get("permissivel", False),
            data.get("justificativa", "Análise automática."),
            data.get("sugestoes", "Nenhuma sugestão.")
        )
    except Exception as e:
        logging.error(f"Falha na API da OpenAI: {e}");
        return fallback, False, "Erro na comunicação com a IA.", ""


# ====================================================================
# Lógica do RODAPÉ (estável)
# ====================================================================
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():
        logging.warning(f"Imagem de assinatura NÃO encontrada em {image_path_str}. A enviar documento original.")
        return None

    logging.info(f"Imagem de assinatura encontrada. A tentar limpar o RODAPÉ e inserir...")

    try:
        doc_stream = io.BytesIO(doc_bytes)
        doc = Document(doc_stream)

        section = doc.sections[0]
        footer = None

        if section.different_first_page_header_footer:
            footer = section.first_page_footer
        else:
            footer = section.footer

        ftr_element = footer._element
        for child in list(ftr_element):
            ftr_element.remove(child)

        paragraph = footer.add_paragraph()
        paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
        run = paragraph.add_run()
        run.add_picture(str(image_path), width=Cm(image_width_cm))

        new_doc_stream = io.BytesIO()
        doc.save(new_doc_stream)

        return new_doc_stream.getvalue()

    except Exception as e:
        logging.error(f"Falha ao adicionar assinatura ao documento Word (no rodapé): {e}", exc_info=True)
        return None

# ====================================================================
# Conversão para PDF via LibreOffice (Linux)
# ====================================================================
def convert_docx_to_pdf_linux(docx_bytes: bytes) -> bytes:
    soffice_cmd = shutil.which("libreoffice") or shutil.which("soffice")
    if not soffice_cmd:
        raise FileNotFoundError("Executável 'libreoffice' ou 'soffice' não encontrado.")

    with tempfile.TemporaryDirectory() as temp_dir:
        temp_dir_path = Path(temp_dir)
        docx_path = temp_dir_path / "input.docx"

        with open(docx_path, "wb") as f:
            f.write(docx_bytes)

        cmd = [
            soffice_cmd,
            "--headless",
            "--convert-to", "pdf",
            "--outdir", temp_dir,
            str(docx_path)
        ]

        try:
            subprocess.run(cmd, capture_output=True, text=True, timeout=30, check=True)
        except subprocess.CalledProcessError as e:
            logging.error(f"Falha na conversão do LibreOffice. Erro: {e.stderr}")
            raise

        pdf_path = temp_dir_path / "input.pdf"
        if not pdf_path.exists():
            raise FileNotFoundError(f"LibreOffice não criou o ficheiro PDF esperado em {pdf_path}")

        with open(pdf_path, "rb") as f:
            pdf_bytes = f.read()

        return pdf_bytes

# ====================================================================
# Função de E-mail (estável)
# ====================================================================
def send_email(
    to_addr: str,
    subject: str,
    body: str,
    attachments: List[Tuple[str, bytes]] = None,
    cc_addr: Optional[str] = None,
    assinatura: str = "",
    sign_image_path: str = "",
    sign_image_width_cm: float = 5.0
):
    if not all([SMTP_HOST, SMTP_USER, SMTP_PASS]): raise ConnectionError("Configurações de SMTP ausentes.")

    msg = EmailMessage()
    msg["From"] = SMTP_USER
    msg["To"] = to_addr
    msg["Subject"] = subject
    if cc_addr:
        msg["Cc"] = cc_addr

    assinatura_html = ""
    image_cid_str_with_brackets = None

    if assinatura:
        assinatura_formatada = assinatura.replace('\n', '<br>')
        assinatura_html = f"<br><br><p>{assinatura_formatada}</p>"

    image_path_obj = Path(sign_image_path)
    if assinatura_html and image_path_obj.is_file():
        try:
            image_cid_str_with_brackets = make_msgid(domain="insutec.ao")
            image_cid_for_html = image_cid_str_with_brackets.strip('<>')
            width_px = int(sign_image_width_cm * 37.795)
            assinatura_html += f'<img src="cid:{image_cid_for_html}" width="{width_px}">'
        except Exception as e:
            logging.warning(f"Erro ao preparar imagem de assinatura: {e}")
            image_cid_str_with_brackets = None

    plain_body = f"{body}\n\n{assinatura}"
    msg.set_content(plain_body, 'plain', 'utf-8')

    body_formatado = body.replace('\n', '<br>')
    html_body = f"""
    <html>
    <body>
        <p>{body_formatado}</p>
        {assinatura_html}
    </body>
    </html>
    """
    msg.add_alternative(html_body, 'html', 'utf-8')


    if image_cid_str_with_brackets and image_path_obj.is_file():
        try:
            img_data = image_path_obj.read_bytes()
            img_part = MIMEImage(img_data)
            img_part.add_header('Content-ID', image_cid_str_with_brackets)
            img_part.add_header('Content-Disposition', 'inline', filename=image_path_obj.name)
            msg.attach(img_part)
        except Exception: pass

    if attachments:
        for fname, fbytes in attachments:
            try:
                if fname.lower().endswith('.txt'):
                    report_string = fbytes.decode('utf-8')
                    txt_part = MIMEText(report_string, 'plain', 'utf-8')
                    txt_part.add_header('Content-Disposition', 'attachment', filename=fname)
                    msg.attach(txt_part)
                else:
                    maintype, subtype = (mimetypes.guess_type(fname)[0] or 'application/octet-stream').split('/')
                    msg.add_attachment(
                        fbytes,
                        maintype=maintype,
                        subtype=subtype,
                        filename=fname
                    )
            except Exception as e:
                logging.error(f"Falha ao anexar ficheiro {fname}: {e}")

    try:
        smtp_class = smtplib.SMTP_SSL if USE_SMTP_SSL else smtplib.SMTP
        with smtp_class(SMTP_HOST, SMTP_PORT) as s:
            if not USE_SMTP_SSL: s.starttls()
            s.login(SMTP_USER, SMTP_PASS)
            s.send_message(msg)
    except Exception as e:
        logging.error(f"Falha ao enviar e-mail para {to_addr}: {e}"); raise

# ====================================================================
# Função principal
# ====================================================================
def process_inbox():
    if not all([IMAP_HOST, IMAP_USER, IMAP_PASS, EMAIL_APROVADO_PARA, EMAIL_CC_GERAL]):
        logging.error("Configurações de IMAP ou E-mail ausentes."); return
    imap = None
    try:
        imap = imaplib.IMAP4_SSL(IMAP_HOST)
        imap.login(IMAP_USER, IMAP_PASS)
        imap.select(f'"{IMAP_LABEL}"')
        status, data = imap.search(None, '(UNSEEN)')
        if status != "OK" or not data[0]:
            return

        email_ids = data[0].split()
        logging.info(f"Encontrados {len(email_ids)} e-mails não lidos para verificar.")

        for num in email_ids:
            try:
                res, raw_full = imap.fetch(num, '(BODY.PEEK[])')
                if res != 'OK': continue

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

                if ASSUNTO_PROVA_PALAVRA_CHAVE.lower() not in subject.lower():
                    continue

                logging.info(f"E-mail de enunciado encontrado: '{subject}'. A processar...")

                from_header = clean_header(msg.get("From", ""))
                sender_name, sender_email = parseaddr(from_header)
                if not sender_email:
                    sender_email = from_header

                docx_parts = []
                for part in msg.walk():
                    filename = clean_header(part.get_filename())
                    if filename and filename.lower().endswith(".docx"):
                        docx_parts.append((filename, part.get_payload(decode=True)))

                if not docx_parts:
                    imap.store(num, '+FLAGS', '\\Seen')
                    continue

                for fname, file_bytes in docx_parts:
                    text = extract_full_docx_text(file_bytes)
                    if not text: continue

                    # 1. Verificação de Profanidade
                    found_profanities = check_for_profanity(text, LISTA_PALAVRAS_INAPROPRIADAS)
                    is_permissible_profanidade = len(found_profanities) == 0

                    # 2. Verificação Ortográfica
                    issues = spelling_issues_pt(text, PALAVRAS_PERSONALIZADAS)
                    is_permissible_ortografia = len(issues) <= MAX_ISSUES

                    # 3. Verificação de Conteúdo (IA + Regex)
                    disciplina, is_permissible_ia, justification, suggestions = evaluate_proof_content(text, DISCIPLINA_FALLBACK)

                    # 4. Decisão Final
                    final_is_permissible = is_permissible_ia and is_permissible_ortografia and is_permissible_profanidade

                    # 5. Relatório
                    report_content = (
                        f"RELATÓRIO DE VALIDAÇÃO AUTOMÁTICA\n"
                        f"====================================\n"
                        f"- Remetente: {from_header}\n"
                        f"- Ficheiro: {fname}\n"
                        f"- Disciplina: {disciplina}\n"
                        f"---\n"
                        f"STATUS: {'ACEITÁVEL' if final_is_permissible else 'REQUER REVISÃO'}\n"
                        f"- Profanidade: {'OK' if is_permissible_profanidade else 'FALHA'}\n"
                        f"- Ortografia: {len(issues)} erros (Max: {MAX_ISSUES})\n"
                        f"- IA/Conteúdo: {'Aprovado' if is_permissible_ia else 'Rejeitado'}\n"
                        f"- Justificativa IA: {justification}\n"
                        f"- Sugestões: {suggestions}\n"
                    )

                    report_attachment = ("relatorio_validacao.txt", report_content.encode('utf-8'))
                    original_doc_attachment = (fname, file_bytes)

                    common_email_args = {
                        "assinatura": ASSINATURA,
                        "sign_image_path": SIGN_IMAGE_PATH,
                        "sign_image_width_cm": SIGN_IMAGE_WIDTH_CM
                    }

                    if final_is_permissible:
                        # Tentar assinar
                        modified_doc_bytes = add_signature_to_doc(file_bytes, SIGN_IMAGE_PATH, SIGN_IMAGE_WIDTH_CM)

                        if not modified_doc_bytes:
                            # Falha na assinatura - Notificar Erro
                            subject_out = f"[ERRO SISTEMA] Enunciado: {disciplina}"
                            body_out = "Aprovado, mas falha técnica ao inserir assinatura."
                            send_email(sender_email, subject_out, body_out, [report_attachment], EMAIL_CC_GERAL, **common_email_args)
                            continue

                        # Converter PDF
                        try:
                            pdf_bytes = convert_docx_to_pdf_linux(modified_doc_bytes)
                            pdf_fname = Path(fname).with_suffix('.pdf').name
                            doc_final = (f"ASSINADO_{pdf_fname}", pdf_bytes)
                        except Exception:
                            doc_final = (f"ASSINADO_{fname}", modified_doc_bytes)

                        # E-mail Direção
                        send_email(
                            EMAIL_APROVADO_PARA,
                            f"[APROVADO] Enunciado: {disciplina}",
                            f"Enunciado de {sender_name} aprovado e assinado.",
                            [doc_final],
                            EMAIL_CC_GERAL,
                            **common_email_args
                        )

                        # E-mail Professor
                        send_email(
                            sender_email,
                            f"[APROVADO] O seu enunciado: {disciplina}",
                            f"O seu enunciado foi aprovado e encaminhado.",
                            [report_attachment, original_doc_attachment],
                            EMAIL_CC_GERAL,
                            **common_email_args
                        )

                    elif REPLY_SENDER_ON_ERROR_FLAG:
                        # Rejeição
                        send_email(
                            sender_email,
                            f"[REVISÃO NECESSÁRIA] Enunciado: {disciplina}",
                            f"O enunciado requer revisão. Ver relatório anexo.",
                            [report_attachment, original_doc_attachment],
                            EMAIL_CC_GERAL,
                            **common_email_args
                        )

                imap.store(num, '+FLAGS', '\\Seen')

            except Exception as e:
                logging.error(f"Erro ao processar e-mail ID {num}: {e}")
                try: imap.store(num, '+FLAGS', '\\Seen')
                except: pass

    finally:
        if imap:
            try: imap.close(); imap.logout()
            except: pass

def main():
    try:
        process_inbox()
    except Exception as e:
        logging.critical(f"Erro fatal: {e}", exc_info=True)
        sys.exit(1)

if __name__ == "__main__":
    logging.info("Serviço iniciado.")
    main()
    logging.info("Serviço finalizado.")
