package services

import (
	"context"
	"errors"
	"fmt"
	"log"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/checker"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/notify"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func monCtx() (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.Background(), 5*time.Second)
}

func SpecFor(m *models.Monitor) checker.Spec {
	return checker.Spec{
		Type:           m.Type,
		URL:            m.Target.URL,
		Host:           m.Target.Host,
		Port:           m.Target.Port,
		Method:         m.Target.Method,
		ExpectedStatus: m.Target.ExpectedStatus,
		Keyword:        m.Target.Keyword,
		TLSWarnDays:    m.Target.TLSWarnDays,
		Insecure:       m.Target.Insecure,
		TimeoutSec:     m.IntervalSec,
	}
}

func ListMonitors(instanceID string) ([]models.Monitor, error) {
	ctx, cancel := monCtx()
	defer cancel()
	cur, err := db.Col("monitors").Find(ctx, bson.M{"instance_id": instanceID}, options.Find().SetSort(bson.M{"created_at": 1}))
	if err != nil {
		return nil, err
	}
	var out []models.Monitor
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func ListMonitorsForRunner(instanceID, runner string) ([]models.Monitor, error) {
	if instanceID == "" {
		return nil, errors.New("instance id required")
	}
	return listMonitorsForRunner(instanceID, runner)
}

func ListServerScheduledMonitors() ([]models.Monitor, error) {
	return listMonitorsForRunner("", models.RunnerServer)
}

func listMonitorsForRunner(instanceID, runner string) ([]models.Monitor, error) {
	ctx, cancel := monCtx()
	defer cancel()
	filter := bson.M{"runner": runner, "enabled": true}
	if instanceID != "" {
		filter["instance_id"] = instanceID
	}
	cur, err := db.Col("monitors").Find(ctx, filter)
	if err != nil {
		return nil, err
	}
	var out []models.Monitor
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func GetMonitor(instanceID, monitorID string) (*models.Monitor, error) {
	ctx, cancel := monCtx()
	defer cancel()
	var m models.Monitor
	err := db.Col("monitors").FindOne(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}).Decode(&m)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &m, nil
}

func getMonitorByID(monitorID string) (*models.Monitor, error) {
	ctx, cancel := monCtx()
	defer cancel()
	var m models.Monitor
	err := db.Col("monitors").FindOne(ctx, bson.M{"monitor_id": monitorID}).Decode(&m)
	if errors.Is(err, mongo.ErrNoDocuments) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return &m, nil
}

func validateRunner(instanceID, runner string) error {
	if runner == "" || runner == models.RunnerServer {
		return nil
	}
	if _, err := GetServer(instanceID, runner); err != nil {
		return fmt.Errorf("runner server %s not found", runner)
	}
	return nil
}

func CreateMonitor(instanceID string, m *models.Monitor) (*models.Monitor, error) {
	ctx, cancel := monCtx()
	defer cancel()
	if err := validateChannelIDs(instanceID, m.ChannelIDs); err != nil {
		return nil, err
	}
	if err := validateRunner(instanceID, m.Runner); err != nil {
		return nil, err
	}
	m.InstanceID = instanceID
	m.MonitorID = uuid.NewString()
	m.CreatedAt = time.Now()
	if m.IntervalSec <= 0 {
		m.IntervalSec = 60
	}
	if m.Retries <= 0 {
		m.Retries = 1
	}
	if m.Runner == "" {
		m.Runner = models.RunnerServer
	}
	m.State = models.MonitorState{Status: models.StatusPending}
	if _, err := db.Col("monitors").InsertOne(ctx, m); err != nil {
		return nil, err
	}
	return m, nil
}

func UpdateMonitor(instanceID, monitorID string, upd bson.M) error {
	ctx, cancel := monCtx()
	defer cancel()

	if raw, present := upd["channel_ids"]; present {
		ids, ok := raw.([]string)
		if !ok {
			return fmt.Errorf("channel_ids must be a string array")
		}
		if err := validateChannelIDs(instanceID, ids); err != nil {
			return err
		}
	}
	if raw, present := upd["runner"]; present {
		runner, ok := raw.(string)
		if !ok {
			return fmt.Errorf("runner must be a string")
		}
		if err := validateRunner(instanceID, runner); err != nil {
			return err
		}

		if runner == "" {
			upd["runner"] = models.RunnerServer
		}
	}
	_, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}, bson.M{"$set": upd})
	return err
}

func DeleteMonitor(instanceID, monitorID string) error {
	ctx, cancel := monCtx()
	defer cancel()
	res, err := db.Col("monitors").DeleteOne(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID})
	if err != nil {
		return err
	}

	if res.DeletedCount == 0 {
		return nil
	}
	db.Col("incidents").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID})
	db.Col("monitor_rollups").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID})
	return nil
}

func ListIncidents(instanceID, monitorID string, limit int64) ([]models.Incident, error) {
	ctx, cancel := monCtx()
	defer cancel()
	if limit <= 0 {
		limit = 50
	}
	cur, err := db.Col("incidents").Find(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID},
		options.Find().SetSort(bson.M{"started_at": -1}).SetLimit(limit))
	if err != nil {
		return nil, err
	}
	var out []models.Incident
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func UptimeRollups(instanceID, monitorID string, since time.Time) ([]models.Rollup, error) {
	ctx, cancel := monCtx()
	defer cancel()
	cur, err := db.Col("monitor_rollups").Find(ctx,
		bson.M{"monitor_id": monitorID, "instance_id": instanceID, "period_start": bson.M{"$gte": since}},
		options.Find().SetSort(bson.M{"period_start": 1}))
	if err != nil {
		return nil, err
	}
	var out []models.Rollup
	if err := cur.All(ctx, &out); err != nil {
		return nil, err
	}
	return out, nil
}

func IngestResult(instanceID, runner, monitorID string, res checker.Result) error {
	if instanceID == "" {
		return errors.New("instance id required")
	}
	return ingestResult(instanceID, runner, monitorID, res)
}

func IngestServerScheduledResult(monitorID string, res checker.Result) error {
	return ingestResult("", models.RunnerServer, monitorID, res)
}

func ingestResult(instanceID, runner, monitorID string, res checker.Result) error {
	ctx, cancel := monCtx()
	defer cancel()

	m, err := getMonitorByID(monitorID)
	if err != nil {
		return err
	}
	if m == nil {
		return fmt.Errorf("monitor %s not found", monitorID)
	}
	if instanceID != "" && m.InstanceID != instanceID {
		return fmt.Errorf("monitor %s belongs to another org", monitorID)
	}
	if m.Runner != runner {
		return fmt.Errorf("monitor %s is not run by %s", monitorID, runner)
	}

	now := time.Now()
	prev := m.State.Status
	retries := m.Retries
	if retries < 1 {
		retries = 1
	}

	newStatus := prev
	fails := m.State.Fails
	if res.Up {
		fails = 0
		newStatus = models.StatusUp
	} else {
		fails++
		if fails >= retries {
			newStatus = models.StatusDown
		} else if prev == "" || prev == models.StatusPending {
			newStatus = models.StatusPending
		}
	}

	state := bson.M{
		"state.status":        newStatus,
		"state.last_check_at": now,
		"state.latency_ms":    res.LatencyMs,
		"state.message":       res.Message,
		"state.fails":         fails,
	}
	if res.CertExpiry != nil {
		state["state.cert_expiry_at"] = *res.CertExpiry
	}
	if _, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID}, bson.M{"$set": state}); err != nil {
		return err
	}

	bucket := now.Truncate(time.Hour)
	up := 0
	if res.Up {
		up = 1
	}

	db.Col("monitor_rollups").UpdateOne(ctx,
		bson.M{"monitor_id": monitorID, "period_start": bucket},
		bson.M{
			"$inc":         bson.M{"checks": 1, "up_count": up, "sum_latency": int64(res.LatencyMs)},
			"$setOnInsert": bson.M{"instance_id": m.InstanceID},
		},
		options.UpdateOne().SetUpsert(true))

	if newStatus != prev {
		switch newStatus {
		case models.StatusDown:
			inc := models.Incident{
				InstanceID: m.InstanceID,
				IncidentID: uuid.NewString(),
				MonitorID:  monitorID,
				StartedAt:  now,
				Cause:      res.Message,
			}
			db.Col("incidents").InsertOne(ctx, inc)
			notifyTransition(m, newStatus, res.Message)
		case models.StatusUp:
			if prev == models.StatusDown {
				db.Col("incidents").UpdateOne(ctx,
					bson.M{"monitor_id": monitorID, "instance_id": m.InstanceID, "resolved_at": nil},
					bson.M{"$set": bson.M{"resolved_at": now}})
				notifyTransition(m, newStatus, res.Message)
			}
		}
	}
	return nil
}

func notifyTransition(m *models.Monitor, newStatus, message string) {
	if len(m.ChannelIDs) == 0 {
		return
	}
	channels, err := GetChannels(m.InstanceID, m.ChannelIDs)
	if err != nil {
		log.Printf("notify: load channels for %s: %v", m.MonitorID, err)
		return
	}
	ev := notify.Event{
		MonitorName: m.Name,
		Type:        m.Type,
		OldStatus:   m.State.Status,
		NewStatus:   newStatus,
		Message:     message,
		Time:        time.Now(),
	}
	for _, ch := range channels {
		if !ch.Enabled {
			continue
		}
		go func(c models.NotificationChannel) {
			if err := notify.Dispatch(c, ev); err != nil {
				log.Printf("notify: dispatch to %s (%s): %v", c.Name, c.Type, err)
			}
		}(ch)
	}
	_ = UpdateMonitor(m.InstanceID, m.MonitorID, bson.M{"state.last_notified_at": time.Now()})
}
