package meet

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"net/smtp"
	"path"
	"strings"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/ultisuite/ulti-backend/internal/ai"
	"github.com/ultisuite/ulti-backend/internal/llm"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
	"github.com/ultisuite/ulti-backend/internal/orgpolicy"
)

type TranscriptProcessor struct {
	db     *pgxpool.Pool
	nc     *nextcloud.Client
	policy *orgpolicy.Loader
	llm    *llm.Client
	logger *slog.Logger
}

func NewTranscriptProcessor(db *pgxpool.Pool, nc *nextcloud.Client, policy *orgpolicy.Loader) *TranscriptProcessor {
	return &TranscriptProcessor{
		db:     db,
		nc:     nc,
		policy: policy,
		llm:    llm.NewClient(),
		logger: slog.Default().With("component", "meet-transcript"),
	}
}

type transcriptJobInput struct {
	RoomID             string
	OrganizerUserID    string
	OrganizerEmail     string
	ParticipantEmails  []string
	RawTranscript      string
	Mode               string
	QueuedAudioURL     string
}

func (p *TranscriptProcessor) Handle(ctx context.Context, in transcriptJobInput) error {
	policy, err := p.policy.MeetPolicy(ctx)
	if err != nil {
		return err
	}
	if !policy.TranscriptionEnabled {
		return fmt.Errorf("transcription disabled")
	}

	mode := strings.TrimSpace(in.Mode)
	if mode == "" {
		mode = policy.TranscriptionMode
	}

	status := "completed"
	body := strings.TrimSpace(in.RawTranscript)
	if mode == "queued" && body == "" && strings.TrimSpace(in.QueuedAudioURL) != "" {
		status = "queued"
		body = ""
	}
	if body == "" && status != "queued" {
		return fmt.Errorf("empty transcript")
	}

	participantsJSON, _ := json.Marshal(in.ParticipantEmails)
	metadataJSON, _ := json.Marshal(map[string]any{"queued_audio_url": in.QueuedAudioURL})
	var jobID string
	err = p.db.QueryRow(ctx, `
		INSERT INTO meet_transcript_jobs (
			room_id, organizer_user_id, organizer_email, mode, status,
			raw_transcript, participant_emails, metadata
		) VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
		RETURNING id::text
	`, in.RoomID, nullIfEmpty(in.OrganizerUserID), nullIfEmpty(in.OrganizerEmail), mode, status,
		body, participantsJSON, metadataJSON).Scan(&jobID)
	if err != nil {
		return fmt.Errorf("insert transcript job: %w", err)
	}

	if status == "queued" {
		p.logger.Info("transcript queued for async processing", "job_id", jobID, "room", in.RoomID)
		return nil
	}

	return p.runPostActions(ctx, jobID, policy, in, body)
}

func (p *TranscriptProcessor) runPostActions(
	ctx context.Context,
	jobID string,
	policy orgpolicy.MeetPolicy,
	in transcriptJobInput,
	rawTranscript string,
) error {
	finalText := rawTranscript
	actions := policy.PostActions

	if actions.LLMEnabled {
		summary, provider, model, usage, err := p.summarize(ctx, policy, rawTranscript)
		if err != nil {
			p.logger.Warn("llm summary failed", "error", err, "job_id", jobID)
		} else if strings.TrimSpace(summary) != "" {
			finalText = summary
			if extID, err := ai.ResolveExternalIDByEmail(ctx, p.db, in.OrganizerEmail); err == nil && extID != "" {
				ai.RecordFeatureUsage(ctx, p.db, extID, "ultimeet", model, provider, usage)
			}
		}
	}

	if actions.DriveEnabled && p.nc != nil && strings.TrimSpace(in.OrganizerUserID) != "" {
		if err := p.saveToDrive(ctx, in.OrganizerUserID, actions.DriveFolderPath, in.RoomID, finalText); err != nil {
			p.logger.Warn("drive save failed", "error", err, "job_id", jobID)
		}
	}
	if actions.LLMEnabled && actions.LLMThenDrive && p.nc != nil && strings.TrimSpace(in.OrganizerUserID) != "" {
		if err := p.saveToDrive(ctx, in.OrganizerUserID, actions.DriveFolderPath, in.RoomID+"-raw", rawTranscript); err != nil {
			p.logger.Warn("drive raw save failed", "error", err, "job_id", jobID)
		}
	}

	emailBody := finalText
	sendEmail := actions.EmailEnabled
	if actions.LLMEnabled && actions.LLMThenEmail {
		sendEmail = true
	}
	if sendEmail {
		recipients := p.resolveRecipients(actions, in)
		if len(recipients) > 0 {
			if err := p.sendOrgEmail(ctx, recipients, "Transcription UltiMeet — "+in.RoomID, emailBody); err != nil {
				p.logger.Warn("transcript email failed", "error", err, "job_id", jobID)
			}
		}
	}

	_, err := p.db.Exec(ctx, `
		UPDATE meet_transcript_jobs
		SET processed_transcript = $2, status = 'completed', updated_at = NOW()
		WHERE id = $3::uuid
	`, jobID, finalText, jobID)
	return err
}

func (p *TranscriptProcessor) summarize(ctx context.Context, policy orgpolicy.MeetPolicy, transcript string) (string, llm.Provider, string, llm.UsageDetail, error) {
	provider, model, err := p.resolveLLM(ctx, policy.PostActions.LLMProviderID)
	if err != nil {
		return "", llm.Provider{}, "", llm.UsageDetail{}, err
	}
	prompt := strings.TrimSpace(policy.PostActions.LLMPrompt)
	if prompt == "" {
		prompt = "Résume cette réunion en français."
	}
	result, err := p.llm.CompleteWithUsage(ctx, provider, model, prompt, transcript)
	if err != nil {
		return "", provider, model, llm.UsageDetail{}, err
	}
	return result.Content, provider, result.Model, result.Usage, nil
}

func (p *TranscriptProcessor) resolveLLM(ctx context.Context, providerID string) (llm.Provider, string, error) {
	var raw []byte
	err := p.db.QueryRow(ctx, `SELECT settings FROM org_settings WHERE id = 1`).Scan(&raw)
	if err != nil {
		return llm.Provider{}, "", err
	}
	stored := map[string]any{}
	if err := json.Unmarshal(raw, &stored); err != nil {
		return llm.Provider{}, "", err
	}
	llmSection, _ := stored["llm"].(map[string]any)
	providersRaw, _ := llmSection["providers"].([]any)
	defaultID, _ := llmSection["default_provider_id"].(string)
	targetID := strings.TrimSpace(providerID)
	if targetID == "" {
		targetID = defaultID
	}
	for _, item := range providersRaw {
		pm, ok := item.(map[string]any)
		if !ok {
			continue
		}
		id, _ := pm["id"].(string)
		if id != targetID {
			continue
		}
		return llm.Provider{
			ID:           id,
			BaseURL:      stringValue(pm["base_url"]),
			APIKey:       stringValue(pm["api_key"]),
			DefaultModel: stringValue(pm["default_model"]),
		}, stringValue(pm["default_model"]), nil
	}
	return llm.Provider{}, "", fmt.Errorf("llm provider not found")
}

func (p *TranscriptProcessor) saveToDrive(ctx context.Context, userID, folderPath, roomID, content string) error {
	folder := strings.TrimSpace(folderPath)
	if folder == "" {
		folder = "/UltiMeet/Transcripts"
	}
	if !strings.HasPrefix(folder, "/") {
		folder = "/" + folder
	}
	fileName := fmt.Sprintf("%s-%s.txt", sanitizeFileName(roomID), time.Now().UTC().Format("20060102-150405"))
	davPath := path.Join(folder, fileName)
	return p.nc.Upload(ctx, userID, davPath, strings.NewReader(content), "text/plain; charset=utf-8")
}

func (p *TranscriptProcessor) resolveRecipients(actions orgpolicy.MeetPostActions, in transcriptJobInput) []string {
	out := make([]string, 0, 8)
	seen := map[string]struct{}{}
	add := func(email string) {
		e := strings.ToLower(strings.TrimSpace(email))
		if e == "" {
			return
		}
		if _, ok := seen[e]; ok {
			return
		}
		seen[e] = struct{}{}
		out = append(out, e)
	}
	switch actions.EmailRecipients {
	case "participants":
		for _, e := range in.ParticipantEmails {
			add(e)
		}
	case "both":
		add(in.OrganizerEmail)
		for _, e := range in.ParticipantEmails {
			add(e)
		}
	case "custom":
		for part := range strings.SplitSeq(actions.EmailCustomAddresses, ",") {
			add(part)
		}
	default:
		add(in.OrganizerEmail)
	}
	return out
}

func (p *TranscriptProcessor) sendOrgEmail(ctx context.Context, to []string, subject, body string) error {
	var raw []byte
	if err := p.db.QueryRow(ctx, `SELECT settings FROM org_settings WHERE id = 1`).Scan(&raw); err != nil {
		return err
	}
	stored := map[string]any{}
	if err := json.Unmarshal(raw, &stored); err != nil {
		return err
	}
	mailing, _ := stored["mailing"].(map[string]any)
	if mailing == nil || !boolValue(mailing["enabled"]) {
		return fmt.Errorf("org mailing disabled")
	}
	host := stringValue(mailing["smtp_host"])
	port := intValue(mailing["smtp_port"], 587)
	user := stringValue(mailing["smtp_user"])
	pass := stringValue(mailing["smtp_password"])
	from := stringValue(mailing["from_email"])
	fromName := stringValue(mailing["from_name"])
	if from == "" {
		return fmt.Errorf("mailing from_email missing")
	}
	addr := fmt.Sprintf("%s:%d", host, port)
	msg := buildPlainEmail(from, fromName, to, subject, body)
	auth := smtp.PlainAuth("", user, pass, host)
	tlsMode := stringValue(mailing["tls_mode"])
	if tlsMode == "none" {
		return smtp.SendMail(addr, nil, from, to, msg)
	}
	return smtp.SendMail(addr, auth, from, to, msg)
}

func buildPlainEmail(from, fromName string, to []string, subject, body string) []byte {
	fromHeader := from
	if strings.TrimSpace(fromName) != "" {
		fromHeader = fmt.Sprintf("%s <%s>", fromName, from)
	}
	var buf bytes.Buffer
	buf.WriteString("From: " + fromHeader + "\r\n")
	buf.WriteString("To: " + strings.Join(to, ", ") + "\r\n")
	buf.WriteString("Subject: " + subject + "\r\n")
	buf.WriteString("MIME-Version: 1.0\r\n")
	buf.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
	buf.WriteString("\r\n")
	buf.WriteString(body)
	return buf.Bytes()
}

func sanitizeFileName(s string) string {
	s = strings.Map(func(r rune) rune {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
			return r
		default:
			return '-'
		}
	}, s)
	if s == "" {
		return "room"
	}
	return s
}

func nullIfEmpty(s string) any {
	if strings.TrimSpace(s) == "" {
		return nil
	}
	return s
}

func boolValue(v any) bool {
	b, _ := v.(bool)
	return b
}

func stringValue(v any) string {
	s, _ := v.(string)
	return s
}

func intValue(v any, fallback int) int {
	switch t := v.(type) {
	case float64:
		if t > 0 {
			return int(t)
		}
	case int:
		if t > 0 {
			return t
		}
	}
	return fallback
}
