#!/usr/bin/env python3
"""
CAFA 6 - Baseline Submission Generator (Kaggle)
===============================================

Gera submissão baseline usando ESM-2 para a competição CAFA 6.
Formato: PROTEIN_ID\tGO_TERM\tSCORE (TSV sem header)

Author: Henrique
Date: 2025-10-27
"""

import os
import torch
import torch.nn as nn
import pandas as pd
import numpy as np
from pathlib import Path
from Bio import SeqIO
import warnings
warnings.filterwarnings('ignore')

# Auto-instalar dependências
try:
    import esm
except ImportError:
    os.system("pip install fair-esm")
    import esm

try:
    from Bio import SeqIO
except ImportError:
    os.system("pip install biopython")
    from Bio import SeqIO

print("🚀 CAFA 6 - Baseline Submission Generator")
print("=" * 60)

# Configuração
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"🖥️  Device: {device}")

# Caminhos dos dados
data_dir = Path('/kaggle/input/cafa-6-protein-function-prediction')
test_fasta = data_dir / 'Test/testsuperset.fasta' 
train_terms = data_dir / 'Train/train_terms.tsv'

print(f"📂 Dados da competição:")
print(f"   - Test FASTA: {test_fasta}")
print(f"   - Train terms: {train_terms}")

class BaselinePredictor:
    """Preditor baseline usando ESM-2"""
    
    def __init__(self, model_name="esm2_t12_35M_UR50D"):
        print(f"🔄 Carregando ESM-2: {model_name}")
        
        # Carregar ESM-2
        self.esm_model, self.esm_alphabet = esm.pretrained.load_model_and_alphabet(model_name)
        self.esm_batch_converter = self.esm_alphabet.get_batch_converter()
        self.esm_model = self.esm_model.to(device)
        self.esm_model.eval()
        
        print(f"✅ ESM-2 carregado - Embedding dim: {self.esm_model.embed_dim}")
        
        # Carregar vocabulário GO dos dados de treino
        self._load_go_vocabulary()
    
    def _load_go_vocabulary(self):
        """Carrega vocabulário GO dos dados de treino"""
        print("📚 Carregando vocabulário GO...")
        
        terms_df = pd.read_csv(train_terms, sep='\t')
        
        # Vocabulários por aspect
        self.vocabs = {}
        for aspect_code, aspect_name in [('F', 'MF'), ('P', 'BP'), ('C', 'CC')]:
            aspect_terms = terms_df[terms_df['aspect'] == aspect_code]['term'].unique()
            self.vocabs[aspect_name] = sorted(aspect_terms)
        
        print(f"✅ Vocabulário GO carregado:")
        print(f"   - MF: {len(self.vocabs['MF']):,} termos")
        print(f"   - BP: {len(self.vocabs['BP']):,} termos")
        print(f"   - CC: {len(self.vocabs['CC']):,} termos")
        print(f"   - Total: {sum(len(v) for v in self.vocabs.values()):,} termos")
    
    def predict_protein(self, protein_id, sequence, threshold=0.3, max_terms_per_protein=1500):
        """Predição baseline para uma proteína"""
        
        # Truncar sequência se muito longa
        if len(sequence) > 1024:
            sequence = sequence[:1024]
        
        # Extrair embeddings ESM-2
        batch_data = [(protein_id, sequence)]
        batch_labels, batch_strs, batch_tokens = self.esm_batch_converter(batch_data)
        batch_tokens = batch_tokens.to(device)
        
        with torch.no_grad():
            results = self.esm_model(batch_tokens, repr_layers=[self.esm_model.num_layers])
            embedding = results["representations"][self.esm_model.num_layers][0, 0]  # CLS token
        
        # Gerar predições baseline (hash determinístico)
        predictions = []
        
        # Hash do embedding para reprodutibilidade
        embedding_hash = hash(tuple(embedding.cpu().numpy().astype(np.float16)))
        
        for aspect_name, terms in self.vocabs.items():
            for go_term in terms:
                # Score baseado em hash determinístico
                term_hash = hash(go_term)
                combined_hash = hash((embedding_hash, term_hash))
                
                # Normalizar para [0, 1] e ajustar distribuição
                score = (abs(combined_hash) % 10000) / 10000.0
                score = score ** 2.5  # Enviesado para scores baixos (mais realista)
                
                if score > threshold:
                    predictions.append((go_term, score))
        
        # Ordenar por score e limitar
        predictions.sort(key=lambda x: x[1], reverse=True)
        predictions = predictions[:max_terms_per_protein]
        
        return predictions

def generate_submission(max_proteins=None, threshold=0.25):
    """Gera arquivo de submissão completo"""
    
    print(f"\n🚀 Gerando submissão...")
    print(f"   - Threshold: {threshold}")
    print(f"   - Max proteínas: {max_proteins or 'Todas'}")
    
    # Criar preditor
    predictor = BaselinePredictor()
    
    submissions = []
    protein_count = 0
    
    # Processar test set
    print(f"\n📊 Processando proteínas do test set...")
    
    for record in SeqIO.parse(test_fasta, "fasta"):
        if max_proteins and protein_count >= max_proteins:
            break
        
        # Extrair protein ID (formato específico do CAFA)
        protein_id = record.id.split('|')[1] if '|' in record.id else record.id
        sequence = str(record.seq)
        
        # Gerar predições
        predictions = predictor.predict_protein(protein_id, sequence, threshold)
        
        # Adicionar à submissão
        for go_term, score in predictions:
            submissions.append([protein_id, go_term, f"{score:.3f}"])
        
        protein_count += 1
        
        # Log progresso
        if protein_count % 1000 == 0:
            print(f"   Processadas: {protein_count:,} proteínas")
    
    print(f"\n✅ Predições geradas:")
    print(f"   - Proteínas: {protein_count:,}")
    print(f"   - Predições totais: {len(submissions):,}")
    if protein_count > 0:
        print(f"   - Média pred/proteína: {len(submissions)/protein_count:.1f}")
    
    # Salvar submissão no formato correto (TSV sem header)
    submission_file = "submission.tsv"
    
    with open(submission_file, 'w') as f:
        for row in submissions:
            f.write(f"{row[0]}\t{row[1]}\t{row[2]}\n")
    
    print(f"\n💾 Submissão salva: {submission_file}")
    
    # Validar formato
    validate_submission(submission_file)
    
    return submission_file

def validate_submission(file_path):
    """Valida formato da submissão"""
    print(f"\n🔍 Validando formato da submissão...")
    
    try:
        # Ler arquivo
        with open(file_path, 'r') as f:
            lines = f.readlines()
        
        print(f"   - Total de linhas: {len(lines):,}")
        
        # Validar algumas linhas
        for i, line in enumerate(lines[:5]):
            parts = line.strip().split('\t')
            if len(parts) != 3:
                print(f"❌ Linha {i+1}: formato incorreto")
                return False
            
            protein_id, go_term, score = parts
            
            # Validar score
            try:
                score_float = float(score)
                if not (0 < score_float <= 1.0):
                    print(f"❌ Linha {i+1}: score fora do intervalo (0, 1]")
                    return False
            except ValueError:
                print(f"❌ Linha {i+1}: score não é numérico")
                return False
            
            # Validar GO term
            if not go_term.startswith('GO:'):
                print(f"❌ Linha {i+1}: GO term inválido")
                return False
        
        print("✅ Formato válido!")
        
        # Mostrar exemplos
        print(f"\n📋 Primeiras 5 linhas:")
        for i, line in enumerate(lines[:5]):
            print(f"   {i+1}: {line.strip()}")
        
        return True
        
    except Exception as e:
        print(f"❌ Erro na validação: {e}")
        return False

def main():
    """Execução principal"""
    
    # Configurações
    CONFIG = {
        'threshold': 0.25,      # Threshold baixo para mais predições
        'max_proteins': 5000,   # Limitar para teste (remover para submissão final)
        'test_mode': True       # Se True, processa apenas subset
    }
    
    print(f"\n📊 Configurações:")
    for key, value in CONFIG.items():
        print(f"   - {key}: {value}")
    
    # Gerar submissão
    submission_file = generate_submission(
        max_proteins=CONFIG['max_proteins'] if CONFIG['test_mode'] else None,
        threshold=CONFIG['threshold']
    )
    
    print(f"\n🎉 Submissão pronta!")
    print(f"📁 Arquivo: {submission_file}")
    print(f"📝 Para submeter: Use a interface do Kaggle ou:")
    print(f"   kaggle competitions submit -c cafa-6-protein-function-prediction -f {submission_file} -m 'Baseline ESM-2 Submission'")
    
    # Estatísticas finais
    with open(submission_file, 'r') as f:
        n_lines = sum(1 for _ in f)
    
    print(f"\n📈 Estatísticas finais:")
    print(f"   - Predições totais: {n_lines:,}")
    
    if CONFIG['max_proteins']:
        estimated_full = n_lines * (224309 / CONFIG['max_proteins'])  # 224k proteínas no test set
        print(f"   - Estimativa completa: ~{estimated_full:,.0f} predições")
    
    return submission_file

if __name__ == "__main__":
    try:
        submission_file = main()
        print("\n✅ Script executado com sucesso!")
    except Exception as e:
        print(f"\n❌ Erro: {e}")
        import traceback
        traceback.print_exc()
