package sources

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"time"

	"golang.org/x/time/rate"

	"github.com/salvacybersec/keyhunter/pkg/providers"
	"github.com/salvacybersec/keyhunter/pkg/recon"
)

// DOSpacesScanner enumerates publicly accessible DigitalOcean Spaces by name
// pattern and flags readable objects matching common config-file patterns as
// potential API key exposure vectors.
//
// Credentialless: uses anonymous HTTP to probe public DO Spaces. DO Spaces are
// S3-compatible, so the same XML ListBucketResult format is used.
type DOSpacesScanner struct {
	Registry *providers.Registry
	Limiters *recon.LimiterRegistry
	// BaseURL overrides the DO Spaces endpoint for tests.
	// Default: "https://%s.%s.digitaloceanspaces.com"
	// Must contain two %s placeholders: bucket name and region.
	BaseURL string
	client  *Client
}

// Compile-time assertion.
var _ recon.ReconSource = (*DOSpacesScanner)(nil)

func (d *DOSpacesScanner) Name() string                { return "spaces" }
func (d *DOSpacesScanner) RateLimit() rate.Limit       { return rate.Every(500 * time.Millisecond) }
func (d *DOSpacesScanner) Burst() int                  { return 3 }
func (d *DOSpacesScanner) RespectsRobots() bool        { return false }
func (d *DOSpacesScanner) Enabled(_ recon.Config) bool { return true }

// doRegions are the DigitalOcean Spaces regions to iterate.
var doRegions = []string{"nyc3", "sfo3", "ams3", "sgp1", "fra1"}

func (d *DOSpacesScanner) Sweep(ctx context.Context, _ string, out chan<- recon.Finding) error {
	client := d.client
	if client == nil {
		client = NewClient()
	}
	baseURL := d.BaseURL
	if baseURL == "" {
		baseURL = "https://%s.%s.digitaloceanspaces.com"
	}

	names := bucketNames(d.Registry)
	if len(names) == 0 {
		return nil
	}

	for _, bucket := range names {
		if err := ctx.Err(); err != nil {
			return err
		}

		for _, region := range doRegions {
			if err := ctx.Err(); err != nil {
				return err
			}
			if d.Limiters != nil {
				if err := d.Limiters.Wait(ctx, d.Name(), d.RateLimit(), d.Burst(), false); err != nil {
					return err
				}
			}

			endpoint := fmt.Sprintf(baseURL, bucket, region)
			keys, err := d.listSpace(ctx, client, endpoint)
			if err != nil {
				log.Printf("spaces: bucket %q region %q probe failed (skipping): %v", bucket, region, err)
				continue
			}

			for _, key := range keys {
				if !isConfigFile(key) {
					continue
				}
				out <- recon.Finding{
					Source:     fmt.Sprintf("do://%s/%s", bucket, key),
					SourceType: "recon:spaces",
					Confidence: "medium",
					DetectedAt: time.Now(),
				}
			}
		}
	}
	return nil
}

// listSpace probes a DO Spaces endpoint via HEAD then parses the S3-compatible
// ListBucketResult XML on success.
func (d *DOSpacesScanner) listSpace(ctx context.Context, client *Client, endpoint string) ([]string, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodHead, endpoint, nil)
	if err != nil {
		return nil, err
	}
	resp, err := client.HTTP.Do(req)
	if err != nil {
		return nil, err
	}
	resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, nil
	}

	getReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return nil, err
	}
	getResp, err := client.Do(ctx, getReq)
	if err != nil {
		return nil, err
	}
	defer getResp.Body.Close()

	// DO Spaces uses S3-compatible XML format.
	return parseS3ListXML(getResp.Body)
}
