// Package sitemon checks the reachability of configured targets (ping, http, tcp).
// Each scanner instance monitors from its own site perspective, giving you
// per-site uptime visibility across all your locations.
package sitemon

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"log/slog"
	"net"
	"net/http"
	"net/smtp"
	"os/exec"
	"runtime"
	"strings"
	"time"

	"github.com/cereda-systems/nexus-scanner/internal/config"
	"github.com/cereda-systems/nexus-scanner/internal/db"
)

const modName = "site_monitoring"

// Module runs periodic availability checks and stores results in the DB.
type Module struct {
	site      string
	cfg       config.SiteMonConfig
	alertCfg  config.AlertConfig
	nexusCfg  config.Config
	store     *db.Store
	client    *http.Client
	alertHTTP *http.Client
	// track previous state per check ID to detect transitions
	prevState map[int64]string
}

// New creates a new site monitoring module.
func New(site string, cfg config.SiteMonConfig, alertCfg config.AlertConfig, nexusCfg config.Config, store *db.Store) *Module {
	return &Module{
		site:      site,
		cfg:       cfg,
		alertCfg:  alertCfg,
		nexusCfg:  nexusCfg,
		store:     store,
		prevState: make(map[int64]string),
		alertHTTP: &http.Client{Timeout: 10 * time.Second},
		client: &http.Client{
			Timeout: 10 * time.Second,
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				if len(via) >= 5 {
					return fmt.Errorf("too many redirects")
				}
				return nil
			},
		},
	}
}

func (m *Module) Name() string            { return modName }
func (m *Module) Interval() time.Duration { return m.cfg.Interval }

// Run executes all enabled checks once and stores results.
func (m *Module) Run(ctx context.Context) error {
	checks, err := m.store.ListMonitorChecks()
	if err != nil {
		return fmt.Errorf("list checks: %w", err)
	}
	if len(checks) == 0 {
		return nil
	}

	scanID, err := m.store.BeginScan(modName)
	if err != nil {
		return fmt.Errorf("begin scan: %w", err)
	}

	for _, check := range checks {
		if !check.Enabled {
			continue
		}
		result := m.runCheck(ctx, check)
		if err := m.store.InsertMonitorResult(result); err != nil {
			slog.Error("insert monitor result", "check", check.Name, "err", err)
		}
		slog.Debug("monitor check done",
			"name", check.Name, "status", result.Status, "latency_ms", result.LatencyMS)

		// Detect state transitions and fire alerts.
		if prev, seen := m.prevState[check.ID]; seen && prev != result.Status {
			go m.sendAlert(check, result)
		}
		m.prevState[check.ID] = result.Status
	}

	_ = m.store.EndScan(scanID, nil)
	_ = m.store.CleanupMonitorResults(100)
	return nil
}

func (m *Module) runCheck(ctx context.Context, check db.MonitorCheck) db.MonitorResult {
	result := db.MonitorResult{
		CheckID:   check.ID,
		CheckedAt: time.Now(),
	}

	start := time.Now()
	var checkErr error

	switch check.Type {
	case "ping":
		checkErr = doPing(ctx, check.Target)
	case "http", "https":
		checkErr = doHTTP(ctx, m.client, check.Target)
	case "tcp":
		checkErr = doTCP(ctx, check.Target)
	default:
		checkErr = fmt.Errorf("unknown type: %s", check.Type)
	}

	result.LatencyMS = int(time.Since(start).Milliseconds())
	if checkErr != nil {
		result.Status = "offline"
		result.Error = checkErr.Error()
	} else {
		result.Status = "online"
	}
	return result
}

func doPing(ctx context.Context, target string) error {
	var args []string
	if runtime.GOOS == "windows" {
		args = []string{"-n", "1", "-w", "2000", target}
	} else {
		args = []string{"-c", "1", "-W", "2", target}
	}
	cmd := exec.CommandContext(ctx, "ping", args...)
	out, err := cmd.Output()
	if err != nil {
		return fmt.Errorf("ping: %w", err)
	}
	lower := strings.ToLower(string(out))
	if strings.Contains(lower, "unreachable") ||
		strings.Contains(lower, "timed out") ||
		strings.Contains(lower, "100% loss") ||
		strings.Contains(lower, "100% packet loss") {
		return fmt.Errorf("host unreachable")
	}
	return nil
}

func doHTTP(ctx context.Context, client *http.Client, target string) error {
	if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") {
		target = "https://" + target
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
	if err != nil {
		return err
	}
	req.Header.Set("User-Agent", "NexusScanner/1.0")
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	resp.Body.Close()
	if resp.StatusCode >= 500 {
		return fmt.Errorf("HTTP %d", resp.StatusCode)
	}
	return nil
}

func doTCP(ctx context.Context, target string) error {
	d := net.Dialer{Timeout: 5 * time.Second}
	conn, err := d.DialContext(ctx, "tcp", target)
	if err != nil {
		return err
	}
	conn.Close()
	return nil
}

/* ── Alerts ───────────────────────────────────────────────────────── */

func (m *Module) sendAlert(check db.MonitorCheck, result db.MonitorResult) {
	emoji := "🔴"
	word := "OFFLINE"
	if result.Status == "online" {
		emoji = "✅"
		word = "WIEDER ONLINE"
	}
	subject := fmt.Sprintf("[Nexus Scanner %s] %s %s: %s", m.site, emoji, word, check.Name)
	body := fmt.Sprintf(
		"Standort: %s\nCheck: %s (%s)\nZiel: %s\nStatus: %s\nZeit: %s\n",
		m.site, check.Name, check.Type, check.Target,
		strings.ToUpper(result.Status),
		result.CheckedAt.Format("02.01.2006 15:04:05"),
	)
	if result.Error != "" {
		body += "Fehler: " + result.Error + "\n"
	}

	if m.alertCfg.SMTP.Enabled {
		if err := m.sendMail(subject, body); err != nil {
			slog.Error("alert mail failed", "err", err)
		} else {
			slog.Info("alert mail sent", "check", check.Name, "status", result.Status)
		}
	}

	if m.alertCfg.NexusEnabled && m.nexusCfg.Nexus.URL != "" {
		if err := m.sendNexusAlert(check, result, subject, body); err != nil {
			slog.Error("nexus alert failed", "err", err)
		} else {
			slog.Info("nexus alert sent", "check", check.Name, "status", result.Status)
		}
	}
}

func (m *Module) sendMail(subject, body string) error {
	sc := m.alertCfg.SMTP
	addr := fmt.Sprintf("%s:%d", sc.Host, sc.Port)
	msg := []byte("From: " + sc.From + "\r\n" +
		"To: " + sc.To + "\r\n" +
		"Subject: " + subject + "\r\n" +
		"Content-Type: text/plain; charset=UTF-8\r\n\r\n" +
		body)

	var auth smtp.Auth
	if sc.Username != "" {
		auth = smtp.PlainAuth("", sc.Username, sc.Password, sc.Host)
	}

	// Try STARTTLS first, fall back to plain.
	tlsCfg := &tls.Config{ServerName: sc.Host, InsecureSkipVerify: false}
	c, err := smtp.Dial(addr)
	if err != nil {
		return fmt.Errorf("dial smtp: %w", err)
	}
	defer c.Close()
	if ok, _ := c.Extension("STARTTLS"); ok {
		if err := c.StartTLS(tlsCfg); err != nil {
			return fmt.Errorf("starttls: %w", err)
		}
	}
	if auth != nil {
		if err := c.Auth(auth); err != nil {
			return fmt.Errorf("smtp auth: %w", err)
		}
	}
	if err := c.Mail(sc.From); err != nil {
		return err
	}
	if err := c.Rcpt(sc.To); err != nil {
		return err
	}
	wc, err := c.Data()
	if err != nil {
		return err
	}
	defer wc.Close()
	_, err = wc.Write(msg)
	return err
}

func (m *Module) sendNexusAlert(check db.MonitorCheck, result db.MonitorResult, subject, body string) error {
	payload := map[string]any{
		"site":      m.site,
		"check":     check.Name,
		"type":      check.Type,
		"target":    check.Target,
		"status":    result.Status,
		"latency":   result.LatencyMS,
		"error":     result.Error,
		"timestamp": result.CheckedAt,
		"subject":   subject,
		"body":      body,
	}
	b, _ := json.Marshal(payload)
	url := m.nexusCfg.Nexus.URL + "/api/scanner/alert"
	req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(b))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("X-Scanner-Key", m.nexusCfg.Nexus.APIKey)
	resp, err := m.alertHTTP.Do(req)
	if err != nil {
		return err
	}
	resp.Body.Close()
	if resp.StatusCode >= 400 {
		return fmt.Errorf("nexus returned %d", resp.StatusCode)
	}
	return nil
}
