// Package pii provides a gRPC client for the Python PII detection service.
// It exposes a thin wrapper with graceful degradation: if the service is
// unreachable and fail_open is true, the original text is returned unchanged.
package pii

import (
	"context"
	"fmt"
	"time"

	"go.uber.org/zap"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

	piiv1 "github.com/veylant/ia-gateway/gen/pii/v1"
)

// Entity is a PII entity detected and pseudonymized by the Python service.
type Entity struct {
	EntityType    string
	OriginalValue string
	Pseudonym     string // token of the form [PII:TYPE:UUID]
	Start         int32
	End           int32
	Confidence    float32
	DetectionLayer string
}

// DetectResult holds the anonymized prompt and the entity list for
// later de-pseudonymization of the LLM response.
type DetectResult struct {
	AnonymizedText  string
	Entities        []Entity
	ProcessingTimeMs int64
}

// Client wraps a gRPC connection to the PII detection service.
type Client struct {
	conn     *grpc.ClientConn
	stub     piiv1.PiiServiceClient
	timeout  time.Duration
	failOpen bool
	logger   *zap.Logger
}

// Config holds the configuration for the PII gRPC client.
type Config struct {
	// Address of the PII service (e.g. "localhost:50051").
	Address string
	// Timeout for each RPC call.
	Timeout time.Duration
	// FailOpen: if true, return the original text on service failure instead of erroring.
	FailOpen bool
}

// New creates a Client and establishes a gRPC connection.
// The connection is lazy — the first RPC will trigger the actual dial.
func New(cfg Config, logger *zap.Logger) (*Client, error) {
	if cfg.Timeout == 0 {
		cfg.Timeout = 100 * time.Millisecond
	}

	conn, err := grpc.NewClient(
		cfg.Address,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
	)
	if err != nil {
		return nil, fmt.Errorf("pii: dial %s: %w", cfg.Address, err)
	}

	return &Client{
		conn:     conn,
		stub:     piiv1.NewPiiServiceClient(conn),
		timeout:  cfg.Timeout,
		failOpen: cfg.FailOpen,
		logger:   logger,
	}, nil
}

// Close releases the gRPC connection.
func (c *Client) Close() error {
	return c.conn.Close()
}

// Detect sends text to the PII service for detection and pseudonymization.
// If the call fails and FailOpen is true, it returns the original text with no entities.
// zeroRetention instructs the Python service not to persist the mapping to Redis —
// the pseudonymization tokens are ephemeral and valid only for this request (E4-12).
func (c *Client) Detect(
	ctx context.Context,
	text, tenantID, requestID string,
	enableNER bool,
	zeroRetention bool,
) (*DetectResult, error) {
	callCtx, cancel := context.WithTimeout(ctx, c.timeout)
	defer cancel()

	resp, err := c.stub.Detect(callCtx, &piiv1.PiiRequest{
		Text:      text,
		TenantId:  tenantID,
		RequestId: requestID,
		Options: &piiv1.PiiOptions{
			EnableNer:           enableNER,
			ConfidenceThreshold: 0.65,
			ZeroRetention:       zeroRetention,
		},
	})
	if err != nil {
		if c.failOpen {
			c.logger.Warn("pii service unavailable — fail open, returning original text",
				zap.String("tenant_id", tenantID),
				zap.String("request_id", requestID),
				zap.Error(err),
			)
			return &DetectResult{AnonymizedText: text}, nil
		}
		return nil, fmt.Errorf("pii: detect RPC: %w", err)
	}

	entities := make([]Entity, 0, len(resp.Entities))
	for _, e := range resp.Entities {
		entities = append(entities, Entity{
			EntityType:     e.EntityType,
			OriginalValue:  e.OriginalValue,
			Pseudonym:      e.Pseudonym,
			Start:          e.Start,
			End:            e.End,
			Confidence:     e.Confidence,
			DetectionLayer: e.DetectionLayer,
		})
	}

	return &DetectResult{
		AnonymizedText:   resp.AnonymizedText,
		Entities:         entities,
		ProcessingTimeMs: resp.ProcessingTimeMs,
	}, nil
}
