Untitled

 avatar
unknown
golang
a year ago
31 kB
12
Indexable
package main

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"html/template"
	"mime/multipart"
	"net/smtp"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	pd "github.com/PipedreamHQ/pipedream-go"
	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"
	"gopkg.in/yaml.v3"
)

// Input types from previous steps
type AnalysisResult struct {
	Action               string   `json:"action"`
	Reason               string   `json:"reason"`
	Consecutivo          string   `json:"consecutivo"`
	BoletinRuedaNegocios string   `json:"boletin_rueda_negocios"`
	Filename             string   `json:"filename"`
	FoundTerms           []string `json:"found_terms"`
	UsedWord             string   `json:"used_word"`
	PDFText              *string  `json:"pdf_text,omitempty"`
	PDFPreview           *string  `json:"pdf_text_preview,omitempty"`
}

type DownloadResult struct {
	Action       string   `json:"action"`
	Reason       string   `json:"reason"`
	Consecutivo  string   `json:"consecutivo"`
	Downloads    []string `json:"downloads"`
	BulletinNum  int      `json:"bulletin_num"`
	SuccessCount int      `json:"success_count"`
}

type ZipResult struct {
	Action      string `json:"action"`
	Reason      string `json:"reason"`
	Consecutivo string `json:"consecutivo"`
	ZipPath     string `json:"zip_path"`
	FileCount   int    `json:"file_count"`
}

type SummarizeResult struct {
	Action          string   `json:"action"`
	Reason          string   `json:"reason"`
	FilteredFiles   []string `json:"filtered_files"`
	DocumentCount   int      `json:"document_count"`
	SummaryResponse string   `json:"summary_response"`
	BulletinDate    string   `json:"bulletin_date"`
	ProcessedTypes  []string `json:"processed_types"`
	LatestFTN       string   `json:"latest_ftn"`
	LatestFTP       string   `json:"latest_ftp"`
}

// Simplified YAML structures
type FinalSummary struct {
	ProjectOverview struct {
		ProjectType        string `yaml:"project_type"`
		ContractingEntity  string `yaml:"contracting_entity"`
		ProjectDescription string `yaml:"project_description"`
		EstimatedValue     string `yaml:"estimated_value"`
	} `yaml:"project_overview"`
	ImportantDates struct {
		DeliveryDate      string `yaml:"delivery_date"`
		ContractStartDate string `yaml:"contract_start_date"`
		ContractEndDate   string `yaml:"contract_end_date"`
	} `yaml:"important_dates"`
	DetailedDescription struct {
		MainObjectives []string `yaml:"main_objectives"`
		ScopeOfWork    []string `yaml:"scope_of_work"`
		Deliverables   []string `yaml:"deliverables"`
	} `yaml:"detailed_description"`
	TechnicalAspects struct {
		TechnicalRequirements []string `yaml:"technical_requirements"`
		EquipmentRequirements []string `yaml:"equipment_requirements"`
	} `yaml:"technical_aspects"`
	FinancialAspects struct {
		BudgetDetails      string   `yaml:"budget_details"`
		PaymentTerms       string   `yaml:"payment_terms"`
		GuaranteesRequired []string `yaml:"guarantees_required"`
	} `yaml:"financial_aspects"`
	TeamAndResources struct {
		RequiredProfiles []string `yaml:"required_profiles"`
		TeamSize         string   `yaml:"team_size"`
	} `yaml:"team_and_resources"`
	RequirementsSummary struct {
		EssentialRequirements []string `yaml:"essential_requirements"`
	} `yaml:"requirements_summary"`
	ExecutiveSummary string `yaml:"executive_summary"`
}

type FinalSummaryWrapper struct {
	FinalSummary FinalSummary `yaml:"final_summary"`
}

// Simplified email template data
type EmailTemplateData struct {
	ContactName              string `json:"contact_name"`
	BusinessOpportunityTitle string `json:"business_opportunity_title"`
	GreetingMessage          string `json:"greeting_message"`
	BidDate                  string `json:"bid_date"`
	BulletinNumber           string `json:"bulletin_number"`
	ClientCompany            string `json:"client_company"`
	BrokerCompany            string `json:"broker_company"`
	BiddingModality          string `json:"bidding_modality"`
	ConsecutiveNumber        string `json:"consecutive_number"`
	ProjectType              string `json:"project_type"`
	DocumentCount            int    `json:"document_count"`
	// LLM data
	ContractingEntity     string   `json:"contracting_entity"`
	ProjectDescription    string   `json:"project_description"`
	EstimatedValue        string   `json:"estimated_value"`
	MainObjectives        []string `json:"main_objectives"`
	ScopeOfWork           []string `json:"scope_of_work"`
	Deliverables          []string `json:"deliverables"`
	TechnicalRequirements []string `json:"technical_requirements"`
	EquipmentRequirements []string `json:"equipment_requirements"`
	BudgetDetails         string   `json:"budget_details"`
	PaymentTerms          string   `json:"payment_terms"`
	GuaranteesRequired    []string `json:"guarantees_required"`
	RequiredProfiles      []string `json:"required_profiles"`
	TeamSize              string   `json:"team_size"`
	EssentialRequirements []string `json:"essential_requirements"`
	DeliveryDate          string   `json:"delivery_date"`
	ContractStartDate     string   `json:"contract_start_date"`
	ContractEndDate       string   `json:"contract_end_date"`
	LLMExecutiveSummary   string   `json:"llm_executive_summary"`
	// Contact info
	SenderName     string `json:"sender_name"`
	SenderTitle    string `json:"sender_title"`
	SenderCompany  string `json:"sender_company"`
	ContactEmail   string `json:"contact_email"`
	ContactPhone   string `json:"contact_phone"`
	CompanyAddress string `json:"company_address"`
	CompanyWebsite string `json:"company_website"`
}

type EmailResult struct {
	Action    string `json:"action"`
	Reason    string `json:"reason"`
	MessageID string `json:"message_id,omitempty"`
	ToEmail   string `json:"to_email,omitempty"`
	FromEmail string `json:"from_email,omitempty"`
}

// Simplified HTML template
const emailTemplate = `<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Oportunidad de Negocio</title>
    <style>
        body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f5f5f5; }
        .email-container { max-width: 600px; margin: 0 auto; background-color: #ffffff; border-radius: 10px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
        .header h1 { margin: 0; font-size: 28px; font-weight: bold; }
        .header p { margin: 10px 0 0 0; font-size: 16px; opacity: 0.9; }
        .urgent-banner { background: #ff6b6b; color: white; padding: 15px; text-align: center; font-weight: bold; font-size: 18px; }
        .main-content { padding: 30px; }
        .section { margin-bottom: 25px; padding: 20px; background: #f8f9fa; border-radius: 8px; border-left: 4px solid #667eea; }
        .section h3 { margin: 0 0 15px 0; color: #667eea; font-size: 18px; }
        .info-row { display: flex; justify-content: space-between; margin-bottom: 8px; padding: 5px 0; border-bottom: 1px solid #e9ecef; }
        .info-row:last-child { border-bottom: none; }
        .info-label { font-weight: bold; color: #555; }
        .info-value { color: #333; text-align: right; }
        .list-item { margin-bottom: 8px; padding-left: 15px; position: relative; }
        .list-item::before { content: '•'; position: absolute; left: 0; color: #667eea; font-weight: bold; }
        .greeting { font-size: 16px; margin-bottom: 20px; color: #555; }
        .footer { background: #2c3e50; color: white; padding: 20px; text-align: center; }
        .footer p { margin: 5px 0; font-size: 14px; }
        .footer a { color: #3498db; text-decoration: none; }
    </style>
</head>
<body>
    <div class="email-container">
        <div class="header">
            <h1>đź’Ľ Oportunidad de Negocio</h1>
            <p>{{.BusinessOpportunityTitle}}</p>
        </div>
        
        <div class="urgent-banner">
            đź“… FECHA DE PUJA: {{.BidDate}}
        </div>
        
        <div class="main-content">
            <div class="greeting">
                Estimado/a {{.ContactName}},<br><br>
                {{.GreetingMessage}}
            </div>
            
            <div class="section">
                <h3>đź“‹ InformaciĂłn General</h3>
                <div class="info-row">
                    <span class="info-label">BoletĂ­n NĂşmero:</span>
                    <span class="info-value">{{.BulletinNumber}}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">Empresa:</span>
                    <span class="info-value">{{.ClientCompany}}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">Comisionista:</span>
                    <span class="info-value">{{.BrokerCompany}}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">Modalidad:</span>
                    <span class="info-value">{{.BiddingModality}}</span>
                </div>
                <div class="info-row">
                    <span class="info-label">Consecutivo:</span>
                    <span class="info-value">{{.ConsecutiveNumber}}</span>
                </div>
                {{if .ProjectType}}
                <div class="info-row">
                    <span class="info-label">Tipo de Proyecto:</span>
                    <span class="info-value">{{.ProjectType}}</span>
                </div>
                {{end}}
                {{if .DocumentCount}}
                <div class="info-row">
                    <span class="info-label">Documentos Procesados:</span>
                    <span class="info-value">{{.DocumentCount}}</span>
                </div>
                {{end}}
            </div>
            
            {{if or .ContractingEntity .ProjectDescription .EstimatedValue}}
            <div class="section">
                <h3>đź“‹ Resumen del Proyecto</h3>
                {{if .ContractingEntity}}
                <div class="info-row">
                    <span class="info-label">Entidad Contratante:</span>
                    <span class="info-value">{{.ContractingEntity}}</span>
                </div>
                {{end}}
                {{if .ProjectDescription}}
                <div class="info-row">
                    <span class="info-label">DescripciĂłn:</span>
                    <span class="info-value">{{.ProjectDescription}}</span>
                </div>
                {{end}}
                {{if .EstimatedValue}}
                <div class="info-row">
                    <span class="info-label">Valor Estimado:</span>
                    <span class="info-value">{{.EstimatedValue}}</span>
                </div>
                {{end}}
            </div>
            {{end}}
            
            {{if or .MainObjectives .ScopeOfWork .Deliverables}}
            <div class="section">
                <h3>🎯 Objetivos y Alcance</h3>
                {{if .MainObjectives}}
                <h4 style="color: #667eea; margin-bottom: 10px;">Objetivos Principales:</h4>
                {{range .MainObjectives}}
                <div class="list-item">{{.}}</div>
                {{end}}
                {{end}}
                
                {{if .ScopeOfWork}}
                <h4 style="color: #667eea; margin: 15px 0 10px 0;">Alcance del Trabajo:</h4>
                {{range .ScopeOfWork}}
                <div class="list-item">{{.}}</div>
                {{end}}
                {{end}}
                
                {{if .Deliverables}}
                <h4 style="color: #667eea; margin: 15px 0 10px 0;">Entregables:</h4>
                {{range .Deliverables}}
                <div class="list-item">{{.}}</div>
                {{end}}
                {{end}}
            </div>
            {{end}}
            
            {{if or .TechnicalRequirements .EquipmentRequirements}}
            <div class="section">
                <h3>🔧 Aspectos Técnicos</h3>
                {{if .TechnicalRequirements}}
                <h4 style="color: #667eea; margin-bottom: 10px;">Requisitos Técnicos:</h4>
                {{range .TechnicalRequirements}}
                <div class="list-item">{{.}}</div>
                {{end}}
                {{end}}
                
                {{if .EquipmentRequirements}}
                <h4 style="color: #667eea; margin: 15px 0 10px 0;">Equipamiento Requerido:</h4>
                {{range .EquipmentRequirements}}
                <div class="list-item">{{.}}</div>
                {{end}}
                {{end}}
            </div>
            {{end}}
            
            {{if or .BudgetDetails .PaymentTerms .GuaranteesRequired}}
            <div class="section">
                <h3>đź’° Aspectos Financieros</h3>
                {{if .BudgetDetails}}
                <div class="info-row">
                    <span class="info-label">Detalles del Presupuesto:</span>
                    <span class="info-value">{{.BudgetDetails}}</span>
                </div>
                {{end}}
                {{if .PaymentTerms}}
                <div class="info-row">
                    <span class="info-label">Términos de Pago:</span>
                    <span class="info-value">{{.PaymentTerms}}</span>
                </div>
                {{end}}
                {{if .GuaranteesRequired}}
                <div class="info-row">
                    <span class="info-label">GarantĂ­as Requeridas:</span>
                    <span class="info-value">{{range $index, $guarantee := .GuaranteesRequired}}{{if $index}}, {{end}}{{$guarantee}}{{end}}</span>
                </div>
                {{end}}
            </div>
            {{end}}
            
            {{if or .RequiredProfiles .TeamSize}}
            <div class="section">
                <h3>👥 Equipo y Recursos</h3>
                {{if .TeamSize}}
                <div class="info-row">
                    <span class="info-label">Tamaño del Equipo:</span>
                    <span class="info-value">{{.TeamSize}}</span>
                </div>
                {{end}}
                
                {{if .RequiredProfiles}}
                <h4 style="color: #667eea; margin: 15px 0 10px 0;">Perfiles Requeridos:</h4>
                {{range .RequiredProfiles}}
                <div class="list-item">{{.}}</div>
                {{end}}
                {{end}}
            </div>
            {{end}}
            
            {{if .EssentialRequirements}}
            <div class="section">
                <h3>đź”’ Requisitos Esenciales</h3>
                {{range .EssentialRequirements}}
                <div class="list-item">{{.}}</div>
                {{end}}
            </div>
            {{end}}
            
            {{if .LLMExecutiveSummary}}
            <div class="section">
                <h3>🤖 Resumen Ejecutivo Completo (IA)</h3>
                <div style="font-size: 16px; line-height: 1.6; color: #2c3e50;">
                    {{.LLMExecutiveSummary}}
                </div>
            </div>
            {{end}}
            
            <div style="margin-top: 30px; padding-top: 20px; border-top: 2px solid #ecf0f1;">
                <p>Las fichas técnicas completas están adjuntas a este correo.</p>
                
                <div style="margin-top: 20px; font-size: 16px;">
                    Atentamente,<br>
                    <strong>{{.SenderName}}</strong><br>
                    {{.SenderTitle}}<br>
                    {{.SenderCompany}}
                </div>
            </div>
        </div>
        
        <div class="footer">
            <p>📧 {{.ContactEmail}} | 📞 {{.ContactPhone}}</p>
            <p>🏢 {{.CompanyAddress}}</p>
            <p><a href="{{.CompanyWebsite}}">{{.CompanyWebsite}}</a></p>
        </div>
    </div>
</body>
</html>`

func init() {
	zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}

func main() {
	log.Info().Msg("đź“§ Starting SMTP email sending process")

	// Get environment variables for SMTP
	smtpHost := os.Getenv("SMTP_HOST")
	smtpPort := os.Getenv("SMTP_PORT")
	smtpUsername := os.Getenv("SMTP_USERNAME")
	smtpPassword := os.Getenv("SMTP_PASSWORD")
	toEmail := os.Getenv("TO_EMAIL")
	fromEmail := os.Getenv("FROM_EMAIL")

	if smtpHost == "" || smtpPort == "" || smtpUsername == "" || smtpPassword == "" || toEmail == "" || fromEmail == "" {
		panic("Missing required environment variables: SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, TO_EMAIL, or FROM_EMAIL")
	}

	// Extract data from previous steps
	analysisData := extractAnalysisResult()
	downloadData := extractDownloadResult()
	zipData := extractZipResult()
	summaryData := extractSummaryResult()

	// Check if we should continue
	if zipData.Action != "continue" {
		exportResult(&EmailResult{
			Action: "skip",
			Reason: "Previous steps did not complete successfully",
		})
		return
	}

	// Build email template data
	templateData := buildTemplateData(analysisData, downloadData, summaryData)

	// Send email with attachment
	result := sendSMTPEmail(smtpHost, smtpPort, smtpUsername, smtpPassword, fromEmail, toEmail, templateData, zipData.ZipPath)
	exportResult(result)

	log.Info().
		Str("action", result.Action).
		Str("to", result.ToEmail).
		Msg("âś… Email process complete")
}

func extractAnalysisResult() *AnalysisResult {
	data := pd.Steps["pdf_analysis"].(map[string]interface{})["analysis_result"].(map[string]interface{})

	result := &AnalysisResult{}
	if v, ok := data["action"].(string); ok {
		result.Action = v
	}
	if v, ok := data["reason"].(string); ok {
		result.Reason = v
	}
	if v, ok := data["consecutivo"].(string); ok {
		result.Consecutivo = v
	}
	if v, ok := data["boletin_rueda_negocios"].(string); ok {
		result.BoletinRuedaNegocios = v
	}
	if v, ok := data["filename"].(string); ok {
		result.Filename = v
	}
	if v, ok := data["used_word"].(string); ok {
		result.UsedWord = v
	}
	if v, ok := data["pdf_text"].(string); ok {
		result.PDFText = &v
	}
	if v, ok := data["pdf_text_preview"].(string); ok {
		result.PDFPreview = &v
	}
	if arr, ok := data["found_terms"].([]interface{}); ok {
		for _, term := range arr {
			if s, ok := term.(string); ok {
				result.FoundTerms = append(result.FoundTerms, s)
			}
		}
	}

	return result
}

func extractDownloadResult() *DownloadResult {
	data := pd.Steps["pdf_bulletin_downloader"].(map[string]interface{})["download_result"].(map[string]interface{})

	result := &DownloadResult{}
	if v, ok := data["action"].(string); ok {
		result.Action = v
	}
	if v, ok := data["reason"].(string); ok {
		result.Reason = v
	}
	if v, ok := data["consecutivo"].(string); ok {
		result.Consecutivo = v
	}
	if v, ok := data["bulletin_num"].(float64); ok {
		result.BulletinNum = int(v)
	}
	if v, ok := data["success_count"].(float64); ok {
		result.SuccessCount = int(v)
	}
	if arr, ok := data["downloads"].([]interface{}); ok {
		for _, download := range arr {
			if s, ok := download.(string); ok {
				result.Downloads = append(result.Downloads, s)
			}
		}
	}

	return result
}

func extractZipResult() *ZipResult {
	data := pd.Steps["zip_all"].(map[string]interface{})["zip_result"].(map[string]interface{})

	result := &ZipResult{}
	if v, ok := data["action"].(string); ok {
		result.Action = v
	}
	if v, ok := data["reason"].(string); ok {
		result.Reason = v
	}
	if v, ok := data["consecutivo"].(string); ok {
		result.Consecutivo = v
	}
	if v, ok := data["zip_path"].(string); ok {
		result.ZipPath = v
	}
	if v, ok := data["file_count"].(float64); ok {
		result.FileCount = int(v)
	}

	return result
}

func extractSummaryResult() *SummarizeResult {
	data := pd.Steps["summary_pdfs"].(map[string]interface{})["summarize_result"].(map[string]interface{})

	result := &SummarizeResult{}
	if v, ok := data["action"].(string); ok {
		result.Action = v
	}
	if v, ok := data["reason"].(string); ok {
		result.Reason = v
	}
	if v, ok := data["document_count"].(float64); ok {
		result.DocumentCount = int(v)
	}
	if v, ok := data["summary_response"].(string); ok {
		result.SummaryResponse = v
	}
	if v, ok := data["bulletin_date"].(string); ok {
		result.BulletinDate = v
	}
	if arr, ok := data["filtered_files"].([]interface{}); ok {
		for _, file := range arr {
			if s, ok := file.(string); ok {
				result.FilteredFiles = append(result.FilteredFiles, s)
			}
		}
	}
	if arr, ok := data["processed_types"].([]interface{}); ok {
		for _, t := range arr {
			if s, ok := t.(string); ok {
				result.ProcessedTypes = append(result.ProcessedTypes, s)
			}
		}
	}
	if v, ok := data["latest_ftn"].(string); ok {
		result.LatestFTN = v
	}
	if v, ok := data["latest_ftp"].(string); ok {
		result.LatestFTP = v
	}

	return result
}

func buildTemplateData(analysis *AnalysisResult, download *DownloadResult, summary *SummarizeResult) *EmailTemplateData {
	// Parse YAML summary
	var finalSummary FinalSummary
	var finalSummaryWrapper FinalSummaryWrapper

	if summary.SummaryResponse != "" {
		cleanedYAML := strings.TrimPrefix(summary.SummaryResponse, "```yaml\n")
		cleanedYAML = strings.TrimSuffix(cleanedYAML, "\n```")

		if err := yaml.Unmarshal([]byte(cleanedYAML), &finalSummaryWrapper); err != nil {
			if err := yaml.Unmarshal([]byte(cleanedYAML), &finalSummary); err != nil {
				log.Warn().Err(err).Msg("⚠️ Failed to parse YAML summary")
			} else {
				log.Info().Msg("âś… Successfully parsed final_summary YAML format")
			}
		} else {
			log.Info().Msg("âś… Successfully parsed final_summary wrapper YAML format")
			finalSummary = finalSummaryWrapper.FinalSummary
		}
	}

	// Extract bid date and bidding modality from PDF text
	bidDate := ""
	biddingModality := ""
	if analysis.PDFText != nil {
		bidDate = extractBidDateFromPDF(*analysis.PDFText)
		biddingModality = extractBiddingModalityFromPDF(*analysis.PDFText)
	} else if analysis.PDFPreview != nil {
		bidDate = extractBidDateFromPDF(*analysis.PDFPreview)
		biddingModality = extractBiddingModalityFromPDF(*analysis.PDFPreview)
	}

	// Use LLM-extracted bulletin date if available
	if summary.BulletinDate != "" {
		bidDate = summary.BulletinDate
	}

	// Extract company names from filename
	clientCompany, brokerCompany := extractCompaniesFromFilename(analysis.Filename)

	// Build business opportunity title
	businessOpportunityTitle := ""
	if finalSummary.ProjectOverview.ProjectDescription != "" {
		words := strings.Fields(finalSummary.ProjectOverview.ProjectDescription)
		if len(words) > 0 {
			maxWords := 8
			if len(words) < maxWords {
				maxWords = len(words)
			}
			businessOpportunityTitle = strings.Join(words[:maxWords], " ")
			if len(words) > 8 {
				businessOpportunityTitle += "..."
			}
		}
	} else if analysis.UsedWord != "" {
		businessOpportunityTitle = analysis.UsedWord
	}

	// Determine project type
	projectType := ""
	if len(summary.ProcessedTypes) > 0 {
		if len(summary.ProcessedTypes) == 1 {
			projectType = summary.ProcessedTypes[0]
		} else {
			projectType = strings.Join(summary.ProcessedTypes, " + ")
		}
	}

	// Get environment variables
	contactName := getEnvOrDefault("CONTACT_NAME", "")
	senderName := getEnvOrDefault("SENDER_NAME", "")
	senderTitle := getEnvOrDefault("SENDER_TITLE", "")
	senderCompany := getEnvOrDefault("SENDER_COMPANY", "")
	contactPhone := getEnvOrDefault("CONTACT_PHONE", "")
	companyAddress := getEnvOrDefault("COMPANY_ADDRESS", "")
	companyWebsite := getEnvOrDefault("COMPANY_WEBSITE", "")

	return &EmailTemplateData{
		ContactName:              contactName,
		BusinessOpportunityTitle: businessOpportunityTitle,
		GreetingMessage:          finalSummary.ExecutiveSummary,
		BidDate:                  bidDate,
		BulletinNumber:           fmt.Sprintf("RN-ALCANCE-%s.1", analysis.BoletinRuedaNegocios),
		ClientCompany:            clientCompany,
		BrokerCompany:            brokerCompany,
		BiddingModality:          biddingModality,
		ConsecutiveNumber:        analysis.Consecutivo,
		ProjectType:              projectType,
		DocumentCount:            summary.DocumentCount,
		ContractingEntity:        finalSummary.ProjectOverview.ContractingEntity,
		ProjectDescription:       finalSummary.ProjectOverview.ProjectDescription,
		EstimatedValue:           finalSummary.ProjectOverview.EstimatedValue,
		MainObjectives:           finalSummary.DetailedDescription.MainObjectives,
		ScopeOfWork:              finalSummary.DetailedDescription.ScopeOfWork,
		Deliverables:             finalSummary.DetailedDescription.Deliverables,
		TechnicalRequirements:    finalSummary.TechnicalAspects.TechnicalRequirements,
		EquipmentRequirements:    finalSummary.TechnicalAspects.EquipmentRequirements,
		BudgetDetails:            finalSummary.FinancialAspects.BudgetDetails,
		PaymentTerms:             finalSummary.FinancialAspects.PaymentTerms,
		GuaranteesRequired:       finalSummary.FinancialAspects.GuaranteesRequired,
		RequiredProfiles:         finalSummary.TeamAndResources.RequiredProfiles,
		TeamSize:                 finalSummary.TeamAndResources.TeamSize,
		EssentialRequirements:    finalSummary.RequirementsSummary.EssentialRequirements,
		DeliveryDate:             finalSummary.ImportantDates.DeliveryDate,
		ContractStartDate:        finalSummary.ImportantDates.ContractStartDate,
		ContractEndDate:          finalSummary.ImportantDates.ContractEndDate,
		LLMExecutiveSummary:      finalSummary.ExecutiveSummary,
		SenderName:               senderName,
		SenderTitle:              senderTitle,
		SenderCompany:            senderCompany,
		ContactEmail:             os.Getenv("FROM_EMAIL"),
		ContactPhone:             contactPhone,
		CompanyAddress:           companyAddress,
		CompanyWebsite:           companyWebsite,
	}
}

func extractBidDateFromPDF(pdfText string) string {
	patterns := []string{
		`comprar[áa]\s+el\s+(\w+),\s+(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})`,
		`FECHA\s+DE\s+PUJA:\s*(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})`,
		`(?:fecha|puja|compra).*?(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})`,
	}

	for _, pattern := range patterns {
		re := regexp.MustCompile(pattern)
		if matches := re.FindStringSubmatch(strings.ToLower(pdfText)); len(matches) > 1 {
			return matches[len(matches)-1]
		}
	}
	return ""
}

func extractBiddingModalityFromPDF(pdfText string) string {
	re := regexp.MustCompile(`MODALIDAD\s+DE\s+PUJA\s*[:]?\s*([^\n]+)`)
	if matches := re.FindStringSubmatch(pdfText); len(matches) > 1 {
		return strings.TrimSpace(matches[1])
	}
	return ""
}

func extractCompaniesFromFilename(filename string) (client, broker string) {
	client = "N/A"
	broker = "N/A"

	name := strings.TrimSuffix(filename, filepath.Ext(filename))
	re := regexp.MustCompile(`B\.R\.N\.\s+\d+\s+-\s+(.+)`)
	matches := re.FindStringSubmatch(name)

	if len(matches) < 2 {
		return
	}

	companiesPart := matches[1]
	lastUnderscore := strings.LastIndex(companiesPart, "_")
	if lastUnderscore == -1 {
		return
	}

	client = strings.TrimSpace(companiesPart[:lastUnderscore])
	broker = strings.TrimSpace(companiesPart[lastUnderscore+1:])

	spaceNormalizer := regexp.MustCompile(`\s+`)
	client = spaceNormalizer.ReplaceAllString(client, " ")
	broker = spaceNormalizer.ReplaceAllString(broker, " ")

	return
}

func getEnvOrDefault(key, defaultValue string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return defaultValue
}

func sendSMTPEmail(smtpHost, smtpPort, username, password, fromEmail, toEmail string, templateData *EmailTemplateData, attachmentPath string) *EmailResult {
	log.Info().
		Str("to", toEmail).
		Str("from", fromEmail).
		Msg("📤 Preparing to send SMTP email")

	// Parse and execute template
	tmpl, err := template.New("email").Parse(emailTemplate)
	if err != nil {
		log.Error().Err(err).Msg("❌ Failed to parse email template")
		return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to parse email template: %v", err)}
	}

	var htmlBody bytes.Buffer
	if err := tmpl.Execute(&htmlBody, templateData); err != nil {
		log.Error().Err(err).Msg("❌ Failed to execute email template")
		return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to execute email template: %v", err)}
	}

	// Create email message
	var emailBuffer bytes.Buffer
	writer := multipart.NewWriter(&emailBuffer)
	boundary := writer.Boundary()
	subject := fmt.Sprintf("Oportunidad de Negocio - %s", templateData.BusinessOpportunityTitle)

	headers := fmt.Sprintf("From: BMC Oportunidades <%s>\r\n", fromEmail)
	headers += fmt.Sprintf("To: %s\r\n", toEmail)
	headers += fmt.Sprintf("Subject: =?UTF-8?B?%s?=\r\n", base64.StdEncoding.EncodeToString([]byte(subject)))
	headers += "MIME-Version: 1.0\r\n"
	headers += fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\r\n", boundary)
	headers += "\r\n"

	emailBuffer.WriteString(headers)

	// Add HTML body
	htmlPart, err := writer.CreatePart(map[string][]string{
		"Content-Type":              {"text/html; charset=UTF-8"},
		"Content-Transfer-Encoding": {"base64"},
	})
	if err != nil {
		log.Error().Err(err).Msg("❌ Failed to create HTML part")
		return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to create HTML part: %v", err)}
	}

	encoded := base64.StdEncoding.EncodeToString(htmlBody.Bytes())
	for i := 0; i < len(encoded); i += 76 {
		end := i + 76
		if end > len(encoded) {
			end = len(encoded)
		}
		htmlPart.Write([]byte(encoded[i:end] + "\r\n"))
	}

	// Add attachment if exists
	if attachmentPath != "" && fileExists(attachmentPath) {
		attachmentData, err := os.ReadFile(attachmentPath)
		if err != nil {
			log.Error().Err(err).Str("path", attachmentPath).Msg("❌ Failed to read attachment")
		} else {
			attachmentPart, err := writer.CreatePart(map[string][]string{
				"Content-Type":              {"application/zip"},
				"Content-Transfer-Encoding": {"base64"},
				"Content-Disposition":       {fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(attachmentPath))},
			})
			if err != nil {
				log.Error().Err(err).Msg("❌ Failed to create attachment part")
			} else {
				encoded := base64.StdEncoding.EncodeToString(attachmentData)
				for i := 0; i < len(encoded); i += 76 {
					end := i + 76
					if end > len(encoded) {
						end = len(encoded)
					}
					attachmentPart.Write([]byte(encoded[i:end] + "\r\n"))
				}
				log.Info().Str("filename", filepath.Base(attachmentPath)).Int("size", len(attachmentData)).Msg("📎 Attachment added")
			}
		}
	}

	writer.Close()

	// Send email
	auth := smtp.PlainAuth("", username, password, smtpHost)
	addr := fmt.Sprintf("%s:%s", smtpHost, smtpPort)

	err = smtp.SendMail(addr, auth, fromEmail, []string{toEmail}, emailBuffer.Bytes())
	if err != nil {
		log.Error().Err(err).Msg("❌ Failed to send SMTP email")
		return &EmailResult{Action: "error", Reason: fmt.Sprintf("Failed to send SMTP email: %v", err)}
	}

	log.Info().Str("smtp_server", addr).Msg("âś… Email sent successfully via SMTP")

	return &EmailResult{
		Action:    "continue",
		Reason:    "Email sent successfully via SMTP",
		MessageID: fmt.Sprintf("smtp-%d", len(subject)),
		ToEmail:   toEmail,
		FromEmail: fromEmail,
	}
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}

func exportResult(result *EmailResult) {
	pd.Export("email_result", result)
	log.Info().
		Str("action", result.Action).
		Str("reason", result.Reason).
		Msg("📤 Email result exported")
}
Editor is loading...
Leave a Comment