package idresolver

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"slices"
	"strings"
	"sync"
	"time"

	"os"

	"tangled.org/core/netutil"

	"github.com/bluesky-social/indigo/atproto/identity"
	"github.com/bluesky-social/indigo/atproto/identity/redisdir"
	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/carlmjohnson/versioninfo"
)

type Resolver struct {
	directory identity.Directory
	base      *identity.BaseDirectory
}

func BaseDirectory(plcUrl string) *identity.BaseDirectory {
	base := identity.BaseDirectory{
		PLCURL: plcUrl,
		HTTPClient: http.Client{
			Timeout: time.Second * 10,
			Transport: &http.Transport{
				// did documents are fetched for caller-supplied dids, so the transport refuses
				// non-public addresses; IDRESOLVER_DEV=1 lifts the guard for local plc instances
				DialContext: netutil.SSRFDialer(os.Getenv("IDRESOLVER_DEV") == "1").DialContext,
				// would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad.
				IdleConnTimeout: time.Millisecond * 1000,
				MaxIdleConns:    100,
			},
		},
		Resolver: net.Resolver{
			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
				d := net.Dialer{Timeout: time.Second * 3}
				return d.DialContext(ctx, network, address)
			},
		},
		TryAuthoritativeDNS: true,
		// primary Bluesky PDS instance only supports HTTP resolution method
		SkipDNSDomainSuffixes: []string{".bsky.social"},
		UserAgent:             "indigo-identity/" + versioninfo.Short(),
	}
	return &base
}

func DefaultResolver(plcUrl string) *Resolver {
	base := BaseDirectory(plcUrl)
	cached := identity.NewCacheDirectory(base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5)
	return &Resolver{
		directory: cached,
		base:      base,
	}
}

func RedisDirectory(base *identity.BaseDirectory, url string) (identity.Directory, error) {
	hitTTL := time.Hour * 24
	errTTL := time.Second * 30
	invalidHandleTTL := time.Minute * 5
	return redisdir.NewRedisDirectory(base, url, hitTTL, errTTL, invalidHandleTTL, 10000)
}

func RedisResolver(redisUrl, plcUrl string) (*Resolver, error) {
	base := BaseDirectory(plcUrl)
	directory, err := RedisDirectory(base, redisUrl)
	if err != nil {
		return nil, err
	}
	return &Resolver{
		directory: directory,
		base:      base,
	}, nil
}

type handleResolver interface {
	ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error)
}

func (r *Resolver) ResolveHandle(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
	if hr, ok := r.directory.(handleResolver); ok {
		return hr.ResolveHandle(ctx, handle)
	}
	return r.base.ResolveHandle(ctx, handle)
}

func (r *Resolver) ResolveAtIdentifier(ctx context.Context, input string) (*identity.Identity, error) {
	if did, err := syntax.ParseDID(input); err == nil {
		return r.directory.LookupDID(ctx, did)
	}
	handle, err := syntax.ParseHandle(input)
	if err != nil {
		return nil, fmt.Errorf("not a did or handle: %w", err)
	}
	handle = handle.Normalize()
	did, err := r.ResolveHandle(ctx, handle)
	if err != nil {
		return nil, fmt.Errorf("resolve handle %q: %w", handle, err)
	}
	ident, err := r.directory.LookupDID(ctx, did)
	if err != nil {
		return nil, fmt.Errorf("lookup did for %q: %w", handle, err)
	}
	aka := "at://" + handle.String()
	if !slices.ContainsFunc(ident.AlsoKnownAs, func(s string) bool { return strings.EqualFold(s, aka) }) {
		return nil, fmt.Errorf("handle %q not declared in alsoKnownAs for %s", handle, did)
	}
	return ident, nil
}

func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) {
	id, err := syntax.ParseAtIdentifier(arg)
	if err != nil {
		return nil, err
	}

	return r.directory.Lookup(ctx, id)
}

func (r *Resolver) ResolveIdents(ctx context.Context, idents []string) []*identity.Identity {
	results := make([]*identity.Identity, len(idents))
	var wg sync.WaitGroup

	done := make(chan struct{})
	defer close(done)

	for idx, ident := range idents {
		wg.Add(1)
		go func(index int, id string) {
			defer wg.Done()

			select {
			case <-ctx.Done():
				results[index] = nil
			case <-done:
				results[index] = nil
			default:
				identity, _ := r.ResolveIdent(ctx, id)
				results[index] = identity
			}
		}(idx, ident)
	}

	wg.Wait()
	return results
}

func (r *Resolver) InvalidateIdent(ctx context.Context, arg string) error {
	id, err := syntax.ParseAtIdentifier(arg)
	if err != nil {
		return err
	}

	return r.directory.Purge(ctx, id)
}

func (r *Resolver) Directory() identity.Directory {
	return r.directory
}
