package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	_ "modernc.org/sqlite"
)

var (
	authToken             = os.Getenv("HEALTH_BRIDGE_TOKEN")
	signedIngestSecret    = os.Getenv("HEALTH_BRIDGE_SIGNED_INGEST_SECRET")
	signedIngestDevice    = os.Getenv("HEALTH_BRIDGE_SIGNED_INGEST_DEVICE")
	signedIngestMaxSkew   int
	signedIngestMaxBytes  int
	defaultSubject        = os.Getenv("HEALTH_BRIDGE_DEFAULT_SUBJECT")
)

func init() {
	if authToken == "" {
		authToken = "changeme"
	}
	if signedIngestDevice == "" {
		signedIngestDevice = "pixel9a-chicho"
	}
	if defaultSubject == "" {
		defaultSubject = "chicho"
	}
	signedIngestMaxSkew = 300
	signedIngestMaxBytes = 2 * 1024 * 1024
	if v := os.Getenv("HEALTH_BRIDGE_SIGNED_INGEST_MAX_SKEW_SECONDS"); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			signedIngestMaxSkew = n
		}
	}
	if v := os.Getenv("HEALTH_BRIDGE_SIGNED_INGEST_MAX_BODY_BYTES"); v != "" {
		if n, err := strconv.Atoi(v); err == nil {
			signedIngestMaxBytes = n
		}
	}
}

type Server struct {
	db *sql.DB
	r  *chi.Mux
}

func NewServer(dbPath string) (*Server, error) {
	db, err := sql.Open("sqlite", dbPath+"?_journal_mode=WAL")
	if err != nil {
		return nil, fmt.Errorf("open db: %w", err)
	}
	db.SetMaxOpenConns(1)

	if err := initDB(db); err != nil {
		return nil, fmt.Errorf("init db: %w", err)
	}

	r := chi.NewRouter()
	s := &Server{db: db, r: r}

	r.Get("/health", s.health)
	r.Get("/", s.root)
	r.Post("/ingest_signed", s.ingestSigned)
	r.Get("/summary", s.getSummary)

	return s, nil
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	s.r.ServeHTTP(w, r)
}

func (s *Server) health(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status": "ok",
		"time":   time.Now().UTC().Format(time.RFC3339),
	})
}

func (s *Server) root(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "ok",
		"service": "health-bridge-go",
	})
}

func (s *Server) getSummary(w http.ResponseWriter, r *http.Request) {
	token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
	if token != authToken {
		http.Error(w, "Invalid token", http.StatusUnauthorized)
		return
	}

	days := 7
	if d := r.URL.Query().Get("days"); d != "" {
		if n, err := strconv.Atoi(d); err == nil {
			days = n
		}
	}
	subject := r.URL.Query().Get("subject")
	if subject == "" {
		subject = defaultSubject
	}

	rows, err := s.db.Query(`
		SELECT subject, date, steps, distance_m, floors, active_minutes,
			exercise_minutes, workout_minutes, auto_activity_minutes,
			calories, active_kcal, total_kcal, nutrition_kcal,
			heart_rate_avg, heart_rate_min, heart_rate_max, sleep_minutes,
			sleep_duration_minutes, sleep_awake_minutes, sleep_light_minutes,
			sleep_deep_minutes, sleep_rem_minutes, sleep_efficiency,
			workouts, workout_count, auto_activity_count, weight_kg,
			body_fat_percentage, height_m, protein_g, carbs_g, fat_g,
			hydration_ml, updated_at
		FROM daily_summary
		WHERE subject = ?
		ORDER BY date DESC
		LIMIT ?
	`, subject, days)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	type DaySummary struct {
		Subject              string  `json:"subject"`
		Date                 string  `json:"date"`
		Steps                int     `json:"steps"`
		DistanceM            float64 `json:"distance_m"`
		Floors               float64 `json:"floors"`
		ActiveMinutes        int     `json:"active_minutes"`
		ExerciseMinutes      int     `json:"exercise_minutes"`
		WorkoutMinutes       int     `json:"workout_minutes"`
		AutoActivityMinutes  int     `json:"auto_activity_minutes"`
		Calories             float64 `json:"calories"`
		ActiveKcal           float64 `json:"active_kcal"`
		TotalKcal            float64 `json:"total_kcal"`
		NutritionKcal        float64 `json:"nutrition_kcal"`
		HeartRateAvg         *float64 `json:"heart_rate_avg"`
		HeartRateMin         *float64 `json:"heart_rate_min"`
		HeartRateMax         *float64 `json:"heart_rate_max"`
		SleepMinutes         int     `json:"sleep_minutes"`
		SleepDurationMinutes int     `json:"sleep_duration_minutes"`
		SleepAwakeMinutes    int     `json:"sleep_awake_minutes"`
		SleepLightMinutes    int     `json:"sleep_light_minutes"`
		SleepDeepMinutes     int     `json:"sleep_deep_minutes"`
		SleepRemMinutes      int     `json:"sleep_rem_minutes"`
		SleepEfficiency      *float64 `json:"sleep_efficiency"`
		Workouts             int     `json:"workouts"`
		WorkoutCount         int     `json:"workout_count"`
		AutoActivityCount    int     `json:"auto_activity_count"`
		WeightKg             *float64 `json:"weight_kg"`
		BodyFatPercentage    *float64 `json:"body_fat_percentage"`
		HeightM              *float64 `json:"height_m"`
		ProteinG             float64 `json:"protein_g"`
		CarbsG               float64 `json:"carbs_g"`
		FatG                 float64 `json:"fat_g"`
		HydrationMl          float64 `json:"hydration_ml"`
		UpdatedAt            string  `json:"updated_at"`
	}

	var summaries []DaySummary
	for rows.Next() {
		var s DaySummary
		var hrAvg, hrMin, hrMax, sleepEff, weightKg, bfPct, heightM sql.NullFloat64
		err := rows.Scan(
			&s.Subject, &s.Date, &s.Steps, &s.DistanceM, &s.Floors, &s.ActiveMinutes,
			&s.ExerciseMinutes, &s.WorkoutMinutes, &s.AutoActivityMinutes,
			&s.Calories, &s.ActiveKcal, &s.TotalKcal, &s.NutritionKcal,
			&hrAvg, &hrMin, &hrMax, &s.SleepMinutes,
			&s.SleepDurationMinutes, &s.SleepAwakeMinutes, &s.SleepLightMinutes,
			&s.SleepDeepMinutes, &s.SleepRemMinutes, &sleepEff,
			&s.Workouts, &s.WorkoutCount, &s.AutoActivityCount, &weightKg,
			&bfPct, &heightM, &s.ProteinG, &s.CarbsG, &s.FatG,
			&s.HydrationMl, &s.UpdatedAt,
		)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if hrAvg.Valid {
			s.HeartRateAvg = &hrAvg.Float64
		}
		if hrMin.Valid {
			s.HeartRateMin = &hrMin.Float64
		}
		if hrMax.Valid {
			s.HeartRateMax = &hrMax.Float64
		}
		if sleepEff.Valid {
			s.SleepEfficiency = &sleepEff.Float64
		}
		if weightKg.Valid {
			s.WeightKg = &weightKg.Float64
		}
		if bfPct.Valid {
			s.BodyFatPercentage = &bfPct.Float64
		}
		if heightM.Valid {
			s.HeightM = &heightM.Float64
		}
		summaries = append(summaries, s)
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"subject": subject,
		"days":    summaries,
	})
}

type IngestRequest struct {
	Records  []json.RawMessage `json:"records"`
	Timezone string            `json:"timezone"`
	Subject  string            `json:"subject"`
}

func (s *Server) ingestSigned(w http.ResponseWriter, r *http.Request) {
	if signedIngestSecret == "" {
		http.Error(w, "Signed ingest is not configured", http.StatusServiceUnavailable)
		return
	}

	body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, int64(signedIngestMaxBytes)))
	if err != nil {
		http.Error(w, "Payload too large", http.StatusRequestEntityTooLarge)
		return
	}

	device := r.Header.Get("X-HB-Device")
	nonce := r.Header.Get("X-HB-Nonce")
	timestampStr := r.Header.Get("X-HB-Timestamp")
	signature := r.Header.Get("X-HB-Signature")

	if device != signedIngestDevice || nonce == "" || timestampStr == "" || signature == "" {
		http.Error(w, "Missing or invalid signed ingest headers", http.StatusUnauthorized)
		return
	}

	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
	if err != nil {
		http.Error(w, "Invalid timestamp", http.StatusUnauthorized)
		return
	}

	now := time.Now().Unix()
	if abs(now-timestamp) > int64(signedIngestMaxSkew) {
		http.Error(w, "Timestamp outside allowed window", http.StatusUnauthorized)
		return
	}

	hash := sha256.Sum256(body)
	bodyHash := hex.EncodeToString(hash[:])

	signedPayload := fmt.Sprintf("%s\n%d\n%s\n%s", device, timestamp, nonce, bodyHash)
	expectedMAC := hmac.New(sha256.New, []byte(signedIngestSecret))
	expectedMAC.Write([]byte(signedPayload))
	expected := hex.EncodeToString(expectedMAC.Sum(nil))

	if !hmac.Equal([]byte(signature), []byte(expected)) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}

	// Check nonce uniqueness
	_, err = s.db.Exec(
		"INSERT INTO ingest_nonces (device, nonce, timestamp, received_at) VALUES (?, ?, ?, datetime('now'))",
		device, nonce, timestamp,
	)
	if err != nil {
		http.Error(w, "Replay nonce", http.StatusConflict)
		return
	}

	// Clean up old nonces
	s.db.Exec("DELETE FROM ingest_nonces WHERE received_at < datetime('now', '-1 day')")

	// Parse and store body
	var req IngestRequest
	if err := json.Unmarshal(body, &req); err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	if len(req.Records) == 0 {
		http.Error(w, "No records provided", http.StatusBadRequest)
		return
	}

	tx, err := s.db.Begin()
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer tx.Rollback()

	subject := req.Subject
	if subject == "" {
		subject = defaultSubject
	}

	var affected []string
	var count int

	for _, rawRec := range req.Records {
		var rec map[string]interface{}
		if err := json.Unmarshal(rawRec, &rec); err != nil {
			continue
		}

		recType := getString(rec, "type", "unknown")
		localDate := getString(rec, "local_date")
		if localDate == "" {
			localDate = getString(rec, "date")
		}
		source := getString(rec, "source")
		if source == "" {
			source = getString(rec, "app")
		}
		if source == "" {
			source = getString(rec, "package_name")
		}
		if source == "" {
			source = "health_connect"
		}

		recordKey := getString(rec, "record_key")
		if recordKey == "" {
			recordKey = getString(rec, "id")
		}
		if recordKey == "" {
			hash := sha256.Sum256(rawRec)
			recordKey = fmt.Sprintf("%s:%s:%s", subject, recType, hex.EncodeToString(hash[:8]))
		} else if !strings.HasPrefix(recordKey, subject+":") {
			recordKey = subject + ":" + recordKey
		}

		rec["subject"] = subject
		rec["record_key"] = recordKey
		if localDate != "" {
			rec["local_date"] = localDate
		}
		rec["source"] = source

		recJSON, _ := json.Marshal(rec)
		recJSONStr := string(recJSON)

		_, err = tx.Exec(`
			INSERT INTO raw_data (subject, data_type, record_key, local_date, source, data_json, received_at)
			VALUES (?, ?, ?, ?, ?, ?, datetime('now'))
			ON CONFLICT(record_key) DO UPDATE SET
				subject = excluded.subject,
				data_type = excluded.data_type,
				local_date = excluded.local_date,
				source = excluded.source,
				data_json = excluded.data_json,
				received_at = datetime('now')
		`, subject, recType, recordKey, localDate, source, recJSONStr)
		if err != nil {
			log.Printf("insert error: %v", err)
		} else {
			count++
			if localDate != "" {
				key := subject + ":" + localDate
				found := false
				for _, a := range affected {
					if a == key {
						found = true
						break
					}
				}
				if !found {
					affected = append(affected, key)
				}
			}
		}
	}

	if err := tx.Commit(); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"status":  "ok",
		"records": count,
	})
}

func getString(m map[string]interface{}, keys ...string) string {
	for _, k := range keys {
		if v, ok := m[k].(string); ok && v != "" {
			return v
		}
	}
	return ""
}

func abs(n int64) int64 {
	if n < 0 {
		return -n
	}
	return n
}

func initDB(db *sql.DB) error {
	_, err := db.Exec(`
		CREATE TABLE IF NOT EXISTS raw_data (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			received_at TEXT NOT NULL DEFAULT (datetime('now')),
			subject TEXT NOT NULL DEFAULT 'chicho',
			data_type TEXT NOT NULL,
			record_key TEXT,
			local_date TEXT,
			source TEXT,
			data_json TEXT NOT NULL
		);
		CREATE INDEX IF NOT EXISTS idx_raw_data_type ON raw_data(data_type);
		CREATE INDEX IF NOT EXISTS idx_raw_data_received ON raw_data(received_at);

		CREATE TABLE IF NOT EXISTS ingest_nonces (
			device TEXT NOT NULL,
			nonce TEXT NOT NULL,
			timestamp INTEGER NOT NULL,
			received_at TEXT NOT NULL DEFAULT (datetime('now')),
			PRIMARY KEY (device, nonce)
		);
		CREATE INDEX IF NOT EXISTS idx_ingest_nonces_received ON ingest_nonces(received_at);

		CREATE TABLE IF NOT EXISTS daily_summary (
			subject TEXT NOT NULL DEFAULT 'chicho',
			date TEXT NOT NULL,
			steps INTEGER DEFAULT 0,
			distance_m REAL DEFAULT 0,
			floors REAL DEFAULT 0,
			active_minutes INTEGER DEFAULT 0,
			exercise_minutes INTEGER DEFAULT 0,
			workout_minutes INTEGER DEFAULT 0,
			auto_activity_minutes INTEGER DEFAULT 0,
			calories REAL DEFAULT 0,
			active_kcal REAL DEFAULT 0,
			total_kcal REAL DEFAULT 0,
			nutrition_kcal REAL DEFAULT 0,
			heart_rate_avg REAL,
			heart_rate_min REAL,
			heart_rate_max REAL,
			sleep_minutes INTEGER DEFAULT 0,
			sleep_duration_minutes INTEGER DEFAULT 0,
			sleep_awake_minutes INTEGER DEFAULT 0,
			sleep_light_minutes INTEGER DEFAULT 0,
			sleep_deep_minutes INTEGER DEFAULT 0,
			sleep_rem_minutes INTEGER DEFAULT 0,
			sleep_efficiency REAL,
			workouts INTEGER DEFAULT 0,
			workout_count INTEGER DEFAULT 0,
			auto_activity_count INTEGER DEFAULT 0,
			weight_kg REAL,
			body_fat_percentage REAL,
			height_m REAL,
			protein_g REAL DEFAULT 0,
			carbs_g REAL DEFAULT 0,
			fat_g REAL DEFAULT 0,
			hydration_ml REAL DEFAULT 0,
			updated_at TEXT NOT NULL DEFAULT (datetime('now')),
			PRIMARY KEY (subject, date)
		);

		CREATE UNIQUE INDEX IF NOT EXISTS idx_raw_data_record_key ON raw_data(record_key);
		CREATE INDEX IF NOT EXISTS idx_raw_data_subject_date ON raw_data(subject, local_date);
		CREATE INDEX IF NOT EXISTS idx_daily_summary_subject_date ON daily_summary(subject, date);
	`)
	return err
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "18007"
	}

	dbPath := os.Getenv("DB_PATH")
	if dbPath == "" {
		dbPath = os.ExpandEnv("$HOME/health-bridge-go/health.db")
	}

	// Ensure directory exists
	dir := dbPath
	for i := len(dir) - 1; i >= 0; i-- {
		if dir[i] == '/' {
			dir = dir[:i]
			break
		}
	}
	os.MkdirAll(dir, 0755)

	srv, err := NewServer(dbPath)
	if err != nil {
		log.Fatal(err)
	}

	log.Printf("health-bridge-go listening on :%s", port)
	log.Fatal(http.ListenAndServe(":"+port, srv))
}