package automation

import (
	"context"
	"encoding/json"
	"log/slog"
	"path"
	"strings"
	"time"

	"github.com/jackc/pgx/v5/pgxpool"
	"github.com/ultisuite/ulti-backend/internal/mail/rules"
	"github.com/ultisuite/ulti-backend/internal/mail/webhooks"
	"github.com/ultisuite/ulti-backend/internal/nextcloud"
)

type Dispatcher struct {
	db     *pgxpool.Pool
	rules  *rules.Engine
	hooks  *webhooks.Executor
	logger *slog.Logger
}

func NewDispatcher(db *pgxpool.Pool, rulesEngine *rules.Engine, hookExec *webhooks.Executor) *Dispatcher {
	return &Dispatcher{
		db:     db,
		rules:  rulesEngine,
		hooks:  hookExec,
		logger: slog.Default().With("component", "automation-dispatcher"),
	}
}

type DrivePayload struct {
	FilePath  string
	FileName  string
	MimeType  string
	FileSize  int64
	IsFolder  bool
}

type ContactPayload struct {
	ID     string
	BookID string
	Name   string
	Email  string
	Phone  string
	Org    string
	Labels []string
}

func DrivePayloadFromPath(filePath string, isFolder bool) DrivePayload {
	normalized := nextcloud.NormalizeClientPath(filePath)
	name := path.Base(strings.TrimSuffix(normalized, "/"))
	if name == "" || name == "." {
		name = normalized
	}
	return DrivePayload{
		FilePath: normalized,
		FileName: name,
		IsFolder: isFolder,
	}
}

func (d *Dispatcher) OnMailCreated(ctx context.Context, userID, accountID, messageID string, msg *rules.Message) {
	if d == nil || userID == "" || msg == nil {
		return
	}
	evt := &rules.EventContext{Type: rules.TriggerMessageReceived}
	if msg.AccountID == "" {
		msg.AccountID = accountID
	}
	if msg.ID == "" {
		msg.ID = messageID
	}
	d.runRules(ctx, userID, msg, evt)
	d.dispatchWebhooks(ctx, userID, string(rules.TriggerMessageReceived), evt, msg, accountID, "", "")
}

func (d *Dispatcher) OnDriveEvent(ctx context.Context, externalUserID string, trigger rules.TriggerType, payload DrivePayload) {
	if d == nil || externalUserID == "" {
		return
	}
	userID, err := d.resolveUserID(ctx, externalUserID)
	if err != nil {
		d.logger.Error("resolve user for drive automation", "error", err, "sub", externalUserID)
		return
	}
	evt := driveEventContext(trigger, payload)
	msg := &rules.Message{}
	d.runRules(ctx, userID, msg, evt)
	d.dispatchWebhooks(ctx, userID, string(trigger), evt, msg, "", payload.FilePath, "")
}

func (d *Dispatcher) OnContactEvent(ctx context.Context, externalUserID string, trigger rules.TriggerType, payload ContactPayload) {
	if d == nil || externalUserID == "" {
		return
	}
	userID, err := d.resolveUserID(ctx, externalUserID)
	if err != nil {
		d.logger.Error("resolve user for contact automation", "error", err, "sub", externalUserID)
		return
	}
	evt := contactEventContext(trigger, payload)
	msg := &rules.Message{}
	d.runRules(ctx, userID, msg, evt)
	d.dispatchWebhooks(ctx, userID, string(trigger), evt, msg, "", "", payload.BookID)
}

func (d *Dispatcher) runRules(ctx context.Context, userID string, msg *rules.Message, evt *rules.EventContext) {
	if d.rules == nil {
		return
	}
	if err := d.rules.EvaluateMessageEvent(ctx, userID, msg, evt); err != nil {
		d.logger.Error("rules evaluation", "error", err, "user_id", userID, "trigger", evt.Type)
	}
}

type webhookTemplateRow struct {
	id            string
	eventTypes    []byte
	mailScope     []byte
	driveScope    []byte
	contactsScope []byte
}

func (d *Dispatcher) dispatchWebhooks(
	ctx context.Context,
	userID string,
	eventType string,
	evt *rules.EventContext,
	msg *rules.Message,
	accountID string,
	drivePath string,
	bookID string,
) {
	if d.hooks == nil || d.db == nil {
		return
	}
	rows, err := d.db.Query(ctx, `
		SELECT id, event_types, mail_scope, drive_scope, contacts_scope
		FROM webhook_templates
		WHERE user_id = $1 AND is_active = true
	`, userID)
	if err != nil {
		d.logger.Error("list webhook templates", "error", err)
		return
	}
	defer rows.Close()

	msgCtx := rules.WebhookContextFromEvent(evt, msg)
	for rows.Next() {
		var row webhookTemplateRow
		if err := rows.Scan(&row.id, &row.eventTypes, &row.mailScope, &row.driveScope, &row.contactsScope); err != nil {
			d.logger.Error("scan webhook template", "error", err)
			continue
		}
		if !webhookMatchesEvent(row, eventType) {
			continue
		}
		if !webhookMatchesScope(row, accountID, drivePath, bookID) {
			continue
		}
		if err := d.hooks.Execute(ctx, row.id, msgCtx); err != nil {
			d.logger.Error("webhook dispatch", "template_id", row.id, "error", err)
		}
	}
}

func webhookMatchesEvent(row webhookTemplateRow, eventType string) bool {
	var types []string
	if len(row.eventTypes) > 0 {
		_ = json.Unmarshal(row.eventTypes, &types)
	}
	if len(types) == 0 {
		return false
	}
	for _, t := range types {
		if t == eventType {
			return true
		}
	}
	return false
}

func webhookMatchesScope(row webhookTemplateRow, accountID, drivePath, bookID string) bool {
	var mailScope MailScope
	var driveScope DriveScope
	var contactsScope ContactsScope
	_ = json.Unmarshal(row.mailScope, &mailScope)
	_ = json.Unmarshal(row.driveScope, &driveScope)
	_ = json.Unmarshal(row.contactsScope, &contactsScope)

	if accountID != "" {
		return AllowsMailScope(mailScope, accountID)
	}
	if drivePath != "" {
		return AllowsDriveScope(driveScope, drivePath)
	}
	if bookID != "" {
		return AllowsContactsScope(contactsScope, bookID)
	}
	return true
}

func (d *Dispatcher) resolveUserID(ctx context.Context, externalID string) (string, error) {
	var userID string
	err := d.db.QueryRow(ctx, `SELECT id FROM users WHERE external_id = $1`, externalID).Scan(&userID)
	return userID, err
}

func driveEventContext(trigger rules.TriggerType, p DrivePayload) *rules.EventContext {
	return &rules.EventContext{
		Type:          trigger,
		FolderPath:    path.Dir(p.FilePath),
		DriveFileName: p.FileName,
		DriveFilePath: p.FilePath,
		DriveMimeType: p.MimeType,
		DriveFileSize: p.FileSize,
		DriveIsFolder: p.IsFolder,
	}
}

func contactEventContext(trigger rules.TriggerType, p ContactPayload) *rules.EventContext {
	return &rules.EventContext{
		Type:          trigger,
		ContactLabel:  strings.Join(p.Labels, ", "),
		ContactID:     p.ID,
		ContactName:   p.Name,
		ContactEmail:  p.Email,
		ContactPhone:  p.Phone,
		ContactOrg:    p.Org,
		ContactBookID: p.BookID,
	}
}

func EventDomain(trigger rules.TriggerType) string {
	switch trigger {
	case rules.TriggerDriveFileCreated, rules.TriggerDriveFileUpdated, rules.TriggerDriveFileDeleted,
		rules.TriggerDriveFileMoved, rules.TriggerDriveShareUpdated:
		return "drive"
	case rules.TriggerContactCreated, rules.TriggerContactUpdated, rules.TriggerContactDeleted:
		return "contacts"
	default:
		return "mail"
	}
}

func NowISO() string {
	return time.Now().UTC().Format(time.RFC3339)
}
