package main

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

type AnzoGraphClient struct {
	cfg        Config
	endpoint   string
	authHeader string
	client     *http.Client
}

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

	return &AnzoGraphClient{
		cfg:        cfg,
		endpoint:   endpoint,
		authHeader: authHeader,
		client:     &http.Client{},
	}
}

func (c *AnzoGraphClient) Startup(ctx context.Context) error {
	if err := c.waitReady(ctx); err != nil {
		return err
	}

	if c.cfg.SparqlClearOnStart {
		if err := c.update(ctx, "CLEAR ALL"); err != nil {
			return err
		}
		if err := c.waitReady(ctx); err != nil {
			return err
		}
	}

	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
		}
	}

	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) {
	ctx2, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	form := url.Values{}
	form.Set("query", query)

	req, err := http.NewRequestWithContext(ctx2, http.MethodPost, c.endpoint, strings.NewReader(form.Encode()))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Set("Accept", "application/sparql-results+json")
	if c.authHeader != "" {
		req.Header.Set("Authorization", c.authHeader)
	}

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

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("sparql query failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
	}
	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")
	if c.authHeader != "" {
		req.Header.Set("Authorization", c.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
	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:
		}

		body, err := c.queryWithTimeout(ctx, "ASK WHERE { ?s ?p ?o }", c.cfg.SparqlReadyTimeout)
		if err == nil {
			// Ensure it's JSON, not HTML/text during boot.
			if strings.HasPrefix(strings.TrimSpace(string(body)), "{") {
				return nil
			}
			err = fmt.Errorf("unexpected readiness response: %s", strings.TrimSpace(string(body)))
		}
		lastErr = err
		time.Sleep(c.cfg.SparqlReadyDelay)
	}
	return fmt.Errorf("anzograph not ready at %s: %w", c.endpoint, lastErr)
}
