package main

import (
	"context"
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"time"

	"visualizador_instanciados/backend_go/queryscope"
)

type AnzoGraphClient struct {
	cfg             Config
	endpoint        string
	basicAuthHeader string
	client          *http.Client
	tokenManager    *keycloakTokenManager
}

func NewAnzoGraphClient(cfg Config) *AnzoGraphClient {
	endpoint := cfg.EffectiveSparqlEndpoint()
	client := &http.Client{}
	basicAuthHeader := ""
	if !cfg.UsesExternalSparql() {
		user := strings.TrimSpace(cfg.SparqlUser)
		pass := strings.TrimSpace(cfg.SparqlPass)
		if user != "" && pass != "" {
			token := base64.StdEncoding.EncodeToString([]byte(user + ":" + pass))
			basicAuthHeader = "Basic " + token
		}
	}

	agc := &AnzoGraphClient{
		cfg:             cfg,
		endpoint:        endpoint,
		basicAuthHeader: basicAuthHeader,
		client:          client,
	}
	if cfg.UsesExternalSparql() {
		agc.tokenManager = newKeycloakTokenManager(cfg, client)
	}
	return agc
}

func (c *AnzoGraphClient) Startup(ctx context.Context) error {
	log.Printf(
		"[sparql] startup source_mode=%s endpoint=%s auth_mode=%s load_on_start=%t",
		c.cfg.SparqlSourceMode,
		c.endpoint,
		c.authMode(),
		c.cfg.SparqlLoadOnStart,
	)

	if c.cfg.UsesExternalSparql() {
		tokenCtx, cancel := context.WithTimeout(ctx, c.cfg.SparqlReadyTimeout)
		defer cancel()
		if _, err := c.refreshExternalToken(tokenCtx, "startup"); err != nil {
			return fmt.Errorf("keycloak startup token fetch failed: %w", err)
		}
	}

	if err := c.waitReady(ctx); err != nil {
		return err
	}
	c.logNamedGraphDatasetProbe(ctx, "startup_initial")

	if c.cfg.SparqlLoadOnStart {
		df := strings.TrimSpace(c.cfg.SparqlDataFile)
		if df == "" {
			return fmt.Errorf("SPARQL_LOAD_ON_START=true but SPARQL_DATA_FILE is not set")
		}
		giri := strings.TrimSpace(c.cfg.SparqlGraphIRI)
		if giri != "" {
			if err := c.update(ctx, fmt.Sprintf("LOAD <%s> INTO GRAPH <%s>", df, giri)); err != nil {
				return err
			}
		} else {
			if err := c.update(ctx, fmt.Sprintf("LOAD <%s>", df)); err != nil {
				return err
			}
		}
		if err := c.waitReady(ctx); err != nil {
			return err
		}
		c.logNamedGraphDatasetProbe(ctx, "startup_post_load")
	}

	return nil
}

func (c *AnzoGraphClient) Shutdown(ctx context.Context) error {
	_ = ctx
	return nil
}

func (c *AnzoGraphClient) Query(ctx context.Context, query string) ([]byte, error) {
	return c.queryWithTimeout(ctx, query, c.cfg.SparqlTimeout)
}

func (c *AnzoGraphClient) queryWithTimeout(ctx context.Context, query string, timeout time.Duration) ([]byte, error) {
	resp, _, err := c.queryRequestWithTimeout(ctx, query, timeout)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	return body, nil
}

func (c *AnzoGraphClient) update(ctx context.Context, update string) error {
	ctx2, cancel := context.WithTimeout(ctx, c.cfg.SparqlTimeout)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx2, http.MethodPost, c.endpoint, strings.NewReader(update))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/sparql-update")
	req.Header.Set("Accept", "application/json")
	authHeader, err := c.authorizationHeader(ctx2, "sparql_update")
	if err != nil {
		return err
	}
	if authHeader != "" {
		req.Header.Set("Authorization", authHeader)
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("sparql update failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
	}
	return nil
}

func (c *AnzoGraphClient) waitReady(ctx context.Context) error {
	var lastErr error
	log.Printf(
		"[sparql] readiness_wait_start endpoint=%s retries=%d timeout=%s delay=%s query_scope=named_graphs",
		c.endpoint,
		c.cfg.SparqlReadyRetries,
		c.cfg.SparqlReadyTimeout,
		c.cfg.SparqlReadyDelay,
	)
	for i := 0; i < c.cfg.SparqlReadyRetries; i++ {
		select {
		case <-ctx.Done():
			if lastErr != nil {
				return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr)
			}
			return ctx.Err()
		default:
		}

		var ask sparqlBooleanResponse
		_, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask)
		if err == nil {
			log.Printf("[sparql] readiness_wait_ok endpoint=%s attempt=%d/%d", c.endpoint, i+1, c.cfg.SparqlReadyRetries)
			return nil
		}
		lastErr = err
		log.Printf("[sparql] readiness_wait_retry endpoint=%s attempt=%d/%d err=%v", c.endpoint, i+1, c.cfg.SparqlReadyRetries, err)
		time.Sleep(c.cfg.SparqlReadyDelay)
	}
	return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr)
}

func namedGraphAnyTripleAskQuery() string {
	return queryscope.AskAnyTripleQuery()
}

func (c *AnzoGraphClient) authMode() string {
	switch {
	case c.cfg.UsesExternalSparql():
		return "bearer"
	case strings.HasPrefix(c.basicAuthHeader, "Basic "):
		return "basic"
	default:
		return "none"
	}
}

func (c *AnzoGraphClient) authorizationHeader(ctx context.Context, reason string) (string, error) {
	if !c.cfg.UsesExternalSparql() {
		return c.basicAuthHeader, nil
	}
	if c.tokenManager == nil {
		return "", fmt.Errorf("external sparql mode is enabled but token manager is not configured")
	}
	token, err := c.tokenManager.EnsureToken(ctx, reason)
	if err != nil {
		return "", err
	}
	return "Bearer " + token, nil
}

func (c *AnzoGraphClient) refreshExternalToken(ctx context.Context, reason string) (string, error) {
	if !c.cfg.UsesExternalSparql() {
		return "", nil
	}
	if c.tokenManager == nil {
		return "", fmt.Errorf("external sparql mode is enabled but token manager is not configured")
	}
	return c.tokenManager.Refresh(ctx, reason)
}

func (c *AnzoGraphClient) logNamedGraphDatasetProbe(ctx context.Context, stage string) {
	var ask sparqlBooleanResponse
	metrics, err := c.queryJSONWithTimeout(ctx, namedGraphAnyTripleAskQuery(), c.cfg.SparqlReadyTimeout, &ask)
	if err != nil {
		log.Printf("[sparql] dataset_probe_failed stage=%s endpoint=%s err=%v", stage, c.endpoint, err)
		return
	}

	log.Printf(
		"[sparql] dataset_probe stage=%s endpoint=%s named_graph_has_triples=%t bytes=%d round_trip_time=%s decode_time=%s",
		stage,
		c.endpoint,
		ask.Boolean,
		metrics.ResponseBytes,
		metrics.RoundTripTime.Truncate(time.Millisecond),
		metrics.BodyDecodeTime.Truncate(time.Millisecond),
	)
}
