// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package checker

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
	"time"

	sdk "git.happydns.org/checker-sdk-go/checker"
	"git.happydns.org/happyDomain/model"
)

// The observation provider registry lives in the Apache-2.0 licensed
// checker-sdk-go module, so external plugins can register themselves
// without depending on AGPL code. These wrappers preserve the existing
// happyDomain call sites.

// RegisterObservationProvider registers an observation provider globally.
func RegisterObservationProvider(p happydns.ObservationProvider) {
	sdk.RegisterObservationProvider(p)
}

// GetObservationProvider returns the provider for the given key, or nil.
func GetObservationProvider(key happydns.ObservationKey) happydns.ObservationProvider {
	return sdk.FindObservationProvider(key)
}

// GetObservationProviders returns all registered observation providers.
func GetObservationProviders() map[happydns.ObservationKey]happydns.ObservationProvider {
	return sdk.GetObservationProviders()
}

// ObservationCacheLookup resolves a cached observation for a target+key.
// Returns the raw data and collection time, or an error if not cached.
type ObservationCacheLookup func(target happydns.CheckTarget, key happydns.ObservationKey) (json.RawMessage, time.Time, error)

// ObservationContext provides lazy-loading, cached, thread-safe access to observation data.
// Collected data is serialized to json.RawMessage immediately after collection.
type ObservationContext struct {
	target           happydns.CheckTarget
	opts             happydns.CheckerOptions
	cache            map[happydns.ObservationKey]json.RawMessage
	errors           map[happydns.ObservationKey]error
	mu               sync.RWMutex
	cacheLookup      ObservationCacheLookup // nil = no DB cache
	freshness        time.Duration          // 0 = always collect
	providerOverride map[happydns.ObservationKey]happydns.ObservationProvider
}

// NewObservationContext creates a new ObservationContext for the given target and options.
// cacheLookup and freshness enable cross-checker observation reuse from stored snapshots.
// Pass nil and 0 to disable DB-based caching.
func NewObservationContext(target happydns.CheckTarget, opts happydns.CheckerOptions, cacheLookup ObservationCacheLookup, freshness time.Duration) *ObservationContext {
	return &ObservationContext{
		target:      target,
		opts:        opts,
		cache:       make(map[happydns.ObservationKey]json.RawMessage),
		errors:      make(map[happydns.ObservationKey]error),
		cacheLookup: cacheLookup,
		freshness:   freshness,
	}
}

// SetProviderOverride registers a per-context provider that takes precedence
// over the global registry for the given observation key.  This is used to
// substitute local providers with HTTP-backed ones when an endpoint is configured.
func (oc *ObservationContext) SetProviderOverride(key happydns.ObservationKey, p happydns.ObservationProvider) {
	if oc.providerOverride == nil {
		oc.providerOverride = make(map[happydns.ObservationKey]happydns.ObservationProvider)
	}
	oc.providerOverride[key] = p
}

// getProvider returns the observation provider for the given key, checking
// per-context overrides first, then falling back to the global registry.
func (oc *ObservationContext) getProvider(key happydns.ObservationKey) happydns.ObservationProvider {
	if oc.providerOverride != nil {
		if p, ok := oc.providerOverride[key]; ok {
			return p
		}
	}
	return GetObservationProvider(key)
}

// Get collects observation data for the given key (lazily) and unmarshals it into dest.
// Thread-safe: concurrent calls for the same key will only trigger one collection.
func (oc *ObservationContext) Get(ctx context.Context, key happydns.ObservationKey, dest any) error {
	// Fast path: check cache under read lock.
	oc.mu.RLock()
	if raw, ok := oc.cache[key]; ok {
		oc.mu.RUnlock()
		return json.Unmarshal(raw, dest)
	}
	if err, ok := oc.errors[key]; ok {
		oc.mu.RUnlock()
		return err
	}
	oc.mu.RUnlock()

	// Slow path: acquire write lock and collect.
	oc.mu.Lock()
	defer oc.mu.Unlock()

	// Double-check after acquiring write lock.
	if raw, ok := oc.cache[key]; ok {
		return json.Unmarshal(raw, dest)
	}
	if err, ok := oc.errors[key]; ok {
		return err
	}

	// Try DB cache before collecting fresh data.
	if oc.cacheLookup != nil && oc.freshness > 0 {
		if raw, collectedAt, err := oc.cacheLookup(oc.target, key); err == nil {
			if time.Since(collectedAt) < oc.freshness {
				oc.cache[key] = raw
				return json.Unmarshal(raw, dest)
			}
		}
	}

	provider := oc.getProvider(key)
	if provider == nil {
		err := fmt.Errorf("no observation provider registered for key %q", key)
		oc.errors[key] = err
		return err
	}

	val, err := provider.Collect(ctx, oc.opts)
	if err != nil {
		oc.errors[key] = err
		return err
	}

	raw, err := json.Marshal(val)
	if err != nil {
		err = fmt.Errorf("observation %q: marshal failed: %w", key, err)
		oc.errors[key] = err
		return err
	}

	oc.cache[key] = json.RawMessage(raw)
	return json.Unmarshal(raw, dest)
}

// Data returns all cached observation data as pre-serialized JSON.
func (oc *ObservationContext) Data() map[happydns.ObservationKey]json.RawMessage {
	oc.mu.RLock()
	defer oc.mu.RUnlock()

	data := make(map[happydns.ObservationKey]json.RawMessage, len(oc.cache))
	for k, v := range oc.cache {
		data[k] = v
	}
	return data
}

// HasHTMLReporter returns true if any registered observation provider implements CheckerHTMLReporter.
func HasHTMLReporter() bool {
	for _, p := range sdk.GetObservationProviders() {
		if _, ok := p.(happydns.CheckerHTMLReporter); ok {
			return true
		}
	}
	return false
}

// GetHTMLReport renders an HTML report for the given observation key and raw JSON data.
// Returns (html, true, nil) if the provider supports HTML reports, or ("", false, nil) if not.
func GetHTMLReport(key happydns.ObservationKey, raw json.RawMessage) (string, bool, error) {
	provider := GetObservationProvider(key)
	if provider == nil {
		return "", false, fmt.Errorf("no observation provider registered for key %q", key)
	}

	hr, ok := provider.(happydns.CheckerHTMLReporter)
	if !ok {
		return "", false, nil
	}
	html, err := hr.GetHTMLReport(raw)
	return html, true, err
}

// HasMetricsReporter returns true if any registered observation provider implements CheckerMetricsReporter.
func HasMetricsReporter() bool {
	for _, p := range sdk.GetObservationProviders() {
		if _, ok := p.(happydns.CheckerMetricsReporter); ok {
			return true
		}
	}
	return false
}

// GetMetrics extracts metrics for the given observation key and raw JSON data.
// Returns (metrics, true, nil) if the provider supports metrics, or (nil, false, nil) if not.
func GetMetrics(key happydns.ObservationKey, raw json.RawMessage, collectedAt time.Time) ([]happydns.CheckMetric, bool, error) {
	provider := GetObservationProvider(key)
	if provider == nil {
		return nil, false, fmt.Errorf("no observation provider registered for key %q", key)
	}

	mr, ok := provider.(happydns.CheckerMetricsReporter)
	if !ok {
		return nil, false, nil
	}
	metrics, err := mr.ExtractMetrics(raw, collectedAt)
	return metrics, true, err
}

// GetAllMetrics extracts metrics from all observation keys in a snapshot.
func GetAllMetrics(snap *happydns.ObservationSnapshot) ([]happydns.CheckMetric, error) {
	var allMetrics []happydns.CheckMetric
	for key, raw := range snap.Data {
		metrics, supported, err := GetMetrics(key, raw, snap.CollectedAt)
		if err != nil || !supported {
			continue
		}
		allMetrics = append(allMetrics, metrics...)
	}
	return allMetrics, nil
}
