package main

import (
	"bufio"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"html"
	"io"
	"math/rand"
	"net"
	"net/http"
	"net/url"
	"os"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	utls "github.com/refraction-networking/utls"
	"golang.org/x/text/encoding/charmap"
)

// Daftar User-Agent modern untuk rotasi
var userAgents = []string{
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0",
	"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/119.0",
	"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0",
}

type result struct {
	KW          string   `json:"kw"`
	Suggestions []string `json:"suggestions,omitempty"`
	Error       string   `json:"error,omitempty"`
}

type seedResult struct {
	Seed        string
	Suggestions []string
	Requests    int64
	Errors      int64
}

type appConfig struct {
	HL          string `json:"hl"`
	GeoLock     string `json:"geo_lock"`
	GeoLockOnly bool   `json:"geo_lock_only"`
}

type keywordQueue struct {
	mu     sync.Mutex
	cond   *sync.Cond
	items  []string
	closed bool
}

func newKeywordQueue() *keywordQueue {
	q := &keywordQueue{items: make([]string, 0, 4096)}
	q.cond = sync.NewCond(&q.mu)
	return q
}

func (q *keywordQueue) Push(s string) {
	q.mu.Lock()
	if q.closed {
		q.mu.Unlock()
		return
	}
	q.items = append(q.items, s)
	q.cond.Signal()
	q.mu.Unlock()
}

func (q *keywordQueue) Pop() (string, bool) {
	q.mu.Lock()
	for len(q.items) == 0 && !q.closed {
		q.cond.Wait()
	}
	if len(q.items) == 0 {
		q.mu.Unlock()
		return "", false
	}
	s := q.items[0]
	q.items[0] = ""
	q.items = q.items[1:]
	q.mu.Unlock()
	return s, true
}

func (q *keywordQueue) Close() {
	q.mu.Lock()
	q.closed = true
	q.cond.Broadcast()
	q.mu.Unlock()
}

func (q *keywordQueue) Len() int {
	q.mu.Lock()
	n := len(q.items)
	q.mu.Unlock()
	return n
}

func main() {
	rand.Seed(time.Now().UnixNano())

	configPath := findArgValue("-config", "configs.json")
	cfg, err := loadConfig(configPath)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
	if strings.TrimSpace(cfg.HL) == "" {
		cfg.HL = "id"
	}

	flag.String("config", configPath, "Path to configs.json")
	inputPath := flag.String("input", "kw.txt", "Path to kw.txt")
	outPath := flag.String("out", "suggestion.txt", "Output .txt path")
	hl := flag.String("hl", cfg.HL, "Language code")
	gl := flag.String("gl", cfg.GeoLock, "Geo location (e.g., ID)")
	geoLockOnly := flag.Bool("geo-lock-only", cfg.GeoLockOnly, "Only allow gl to match config geo_lock")
	deep := flag.Bool("deep", true, "Deep crawl")
	max := flag.Int("max", 0, "Max requests per seed; 0=unlimited")
	target := flag.Int("target", 1000, "Target number of unique suggestions per seed")
	includeBase := flag.Bool("include-base", false, "Include the base query")
	alphaSuffix := flag.Bool("alpha", true, "Seed with alphabet suffix queries")
	digitSuffix := flag.Bool("digits", false, "Seed with digit suffix queries")
	concurrency := flag.Int("concurrency", 100, "Number of concurrent requests")
	seedConcurrency := flag.Int("seed-concurrency", 5, "Number of seeds to process in parallel")
	delay := flag.Duration("delay", 250*time.Millisecond, "Delay between requests")
	progressEvery := flag.Int("progress", 25, "Print progress every N processed requests")
	printLines := flag.Bool("print", true, "Print results to stdout")
	seedHeader := flag.Bool("seed-header", false, "Write '# seed:' header")
	proxyFile := flag.String("proxy-file", "proxy.txt", "Path to proxy.txt")
	proxyStr := flag.String("proxy", "", "HTTP proxy")
	jsonOut := flag.Bool("json", false, "Write JSON results to stdout")
	flag.Parse()

	if *concurrency < 1 {
		*concurrency = 1
	}
	if *seedConcurrency < 1 {
		*seedConcurrency = 1
	}

	f, err := os.Open(*inputPath)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
	defer f.Close()

	// 1. Setup HTTP Clients (uTLS + Proxy Rotation)
	var clients []*http.Client
	proxyValue := strings.TrimSpace(*proxyStr)
	proxyValues := make([]string, 0, 8)
	if proxyValue != "" {
		proxyValues = append(proxyValues, proxyValue)
	} else {
		proxyValues, _ = readNonEmptyLines(*proxyFile)
	}

	if len(proxyValues) > 0 {
		for _, pv := range proxyValues {
			pu, err := parseHTTPProxy(pv)
			if err != nil {
				fmt.Fprintln(os.Stderr, err.Error())
				os.Exit(1)
			}
			clients = append(clients, &http.Client{
				Timeout:   20 * time.Second,
				Transport: &http.Transport{DialTLSContext: makeUTLSDialer(pu)},
			})
		}
	} else {
		clients = append(clients, &http.Client{
			Timeout:   20 * time.Second,
			Transport: &http.Transport{DialTLSContext: makeUTLSDialer(nil)},
		})
	}

	scanner := bufio.NewScanner(f)
	scanner.Buffer(make([]byte, 1024), 1024*1024)

	var encoder *json.Encoder
	if *jsonOut {
		encoder = json.NewEncoder(os.Stdout)
		encoder.SetEscapeHTML(false)
	}

	seeds := make([]string, 0, 256)
	seenSeed := map[string]struct{}{}
	for scanner.Scan() {
		for _, kw := range splitKeywords(scanner.Text()) {
			if kw == "" || seenSeed[kw] != struct{}{} {
				seenSeed[kw] = struct{}{}
				seeds = append(seeds, kw)
			}
		}
	}

	var outFile *os.File
	var outWriter *bufio.Writer
	var ioMu sync.Mutex
	if strings.TrimSpace(*outPath) != "" {
		outFile, err = os.Create(*outPath)
		if err != nil {
			fmt.Fprintln(os.Stderr, err.Error())
			os.Exit(1)
		}
		defer outFile.Close()
		outWriter = bufio.NewWriter(outFile)
		defer outWriter.Flush()
	}

	effectiveSeedConcurrency := *seedConcurrency
	if effectiveSeedConcurrency > len(seeds) {
		effectiveSeedConcurrency = len(seeds)
	}
	perSeedConcurrency := *concurrency / effectiveSeedConcurrency
	if perSeedConcurrency < 1 {
		perSeedConcurrency = 1
	}

	seedJobs := make(chan string, effectiveSeedConcurrency*2)
	seedResults := make(chan seedResult, effectiveSeedConcurrency*2)
	var seedWorkers sync.WaitGroup
	seedWorkers.Add(effectiveSeedConcurrency)
	for i := 0; i < effectiveSeedConcurrency; i++ {
		go func() {
			defer seedWorkers.Done()
			for seed := range seedJobs {
				suggestions, reqProcessed, reqErrors := processSeed(seed, clients, *hl, *gl, *target, *max, perSeedConcurrency, *delay, *deep, *progressEvery, *includeBase, *alphaSuffix, *digitSuffix, encoder, &ioMu)
				seedResults <- seedResult{Seed: seed, Suggestions: suggestions, Requests: reqProcessed, Errors: reqErrors}
			}
		}()
	}
	go func() {
		for _, seed := range seeds {
			seedJobs <- seed
		}
		close(seedJobs)
		seedWorkers.Wait()
		close(seedResults)
	}()

	var totalAll int64
	for r := range seedResults {
		if outWriter != nil && *seedHeader {
			ioMu.Lock()
			_, _ = outWriter.WriteString("# seed: " + r.Seed + "\n")
			ioMu.Unlock()
		}
		var perSeedCount int64
		for _, s := range r.Suggestions {
			if outWriter != nil {
				ioMu.Lock()
				_, _ = outWriter.WriteString(s + "\n")
				ioMu.Unlock()
			}
			perSeedCount++
			if *printLines && !*jsonOut {
				ioMu.Lock()
				fmt.Fprintf(os.Stdout, "%d - %s\n", perSeedCount, s)
				ioMu.Unlock()
			}
		}
		totalAll += perSeedCount
		if *jsonOut || !*printLines {
			fmt.Fprintf(os.Stderr, "seed=%q suggestions=%d requests=%d errors=%d\n", r.Seed, perSeedCount, r.Requests, r.Errors)
		}
	}

	if *printLines && !*jsonOut {
		fmt.Fprintf(os.Stdout, "TOTAL_ALL: %d\n", totalAll)
	}
}

// 2. Fungsi uTLS Dialer (Mendukung Koneksi Langsung & HTTP CONNECT Proxy)
func makeUTLSDialer(proxyURL *url.URL) func(context.Context, string, string) (net.Conn, error) {
	return func(ctx context.Context, network, addr string) (net.Conn, error) {
		var rawConn net.Conn
		var err error
		dialer := &net.Dialer{Timeout: 10 * time.Second}

		if proxyURL != nil {
			rawConn, err = dialer.DialContext(ctx, "tcp", proxyURL.Host)
			if err != nil {
				return nil, err
			}
			req := &http.Request{
				Method: "CONNECT",
				URL:    &url.URL{Opaque: addr},
				Host:   addr,
			}
			if proxyURL.User != nil {
				auth := proxyURL.User.String()
				req.Header = http.Header{"Proxy-Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte(auth))}}
			}
			if err = req.Write(rawConn); err != nil {
				rawConn.Close()
				return nil, err
			}
			resp, err := http.ReadResponse(bufio.NewReader(rawConn), req)
			if err != nil || resp.StatusCode != 200 {
				rawConn.Close()
				return nil, fmt.Errorf("proxy connect failed: %v", err)
			}
		} else {
			rawConn, err = dialer.DialContext(ctx, network, addr)
			if err != nil {
				return nil, err
			}
		}

		host, _, _ := net.SplitHostPort(addr)
		uConn := utls.UClient(rawConn, &utls.Config{ServerName: host}, utls.HelloChrome_Auto)
		if err := uConn.HandshakeContext(ctx); err != nil {
			rawConn.Close()
			return nil, err
		}
		return uConn, nil
	}
}

func processSeed(seed string, clients []*http.Client, hl, gl string, target int, maxRequests int, concurrency int, delay time.Duration, deep bool, progressEvery int, includeBase bool, alphaSuffix bool, digitSuffix bool, encoder *json.Encoder, ioMu *sync.Mutex) (suggestions []string, processed int64, errors int64) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	queue := newKeywordQueue()
	resultCh := make(chan result, concurrency*4)

	seen := map[string]struct{}{}
	var seenMu sync.Mutex
	var pending, submitted, printed int64
	suggestions = make([]string, 0, 2048)

	submit := func(q string) {
		q = strings.TrimSpace(q)
		if q == "" {
			return
		}
		atomic.AddInt64(&submitted, 1)
		atomic.AddInt64(&pending, 1)
		queue.Push(q)
	}

	addSuggestion := func(s string) {
		if target > 0 && atomic.LoadInt64(&printed) >= int64(target) {
			return
		}
		seenMu.Lock()
		if _, ok := seen[s]; ok {
			seenMu.Unlock()
			return
		}
		seen[s] = struct{}{}
		seenMu.Unlock()

		n := atomic.AddInt64(&printed, 1)
		suggestions = append(suggestions, s)
		if deep && (target == 0 || n < int64(target)) {
			submit(s)
		}
		if target > 0 && n >= int64(target) {
			cancel()
			queue.Close()
		}
	}

	for _, q := range initialQueries(seed, includeBase, alphaSuffix, digitSuffix) {
		submit(q)
	}

	var workers sync.WaitGroup
	workers.Add(concurrency)
	for i := 0; i < concurrency; i++ {
		go func() {
			defer workers.Done()
			for {
				q, ok := queue.Pop()
				if !ok {
					return
				}
				if delay > 0 {
					time.Sleep(delay)
				}
				if ctx.Err() != nil {
					resultCh <- result{KW: q, Error: ctx.Err().Error()}
					continue
				}

				reqCtx, reqCancel := context.WithTimeout(ctx, 30*time.Second)
				sugs, fetchErr := fetchSuggestions(reqCtx, clients, q, hl, gl)
				reqCancel()

				r := result{KW: q}
				if fetchErr != nil {
					r.Error = fetchErr.Error()
				} else {
					r.Suggestions = sugs
				}
				resultCh <- r
			}
		}()
	}
	go func() {
		workers.Wait()
		close(resultCh)
	}()

	for r := range resultCh {
		processed++
		if r.Error != "" {
			errors++
		}
		if r.Error == "" {
			for _, s := range r.Suggestions {
				addSuggestion(s)
			}
		}
		remaining := atomic.AddInt64(&pending, -1)
		if progressEvery > 0 && processed%int64(progressEvery) == 0 {
			ioMu.Lock()
			fmt.Fprintf(os.Stderr, "seed=%q requests=%d suggestions=%d pending=%d errors=%d\n", seed, processed, atomic.LoadInt64(&printed), remaining, errors)
			ioMu.Unlock()
		}
		if remaining == 0 {
			queue.Close()
			cancel()
		}
	}

	return suggestions, processed, errors
}

// 3. Modifikasi Fetch dengan Exponential Backoff & Rotasi Client/User-Agent
func fetchSuggestions(ctx context.Context, clients []*http.Client, kw, hl, gl string) ([]string, error) {
	endpoint, _ := url.Parse("https://www.google.com/complete/search")
	q := endpoint.Query()
	q.Set("cp", "1")
	q.Set("client", "gws-wiz")
	q.Set("xssi", "t")
	q.Set("hl", hl)
	if strings.TrimSpace(gl) != "" {
		q.Set("gl", gl)
	}
	q.Set("authuser", "0")
	q.Set("q", kw)
	endpoint.RawQuery = q.Encode()

	var lastErr error
	backoff := 500 * time.Millisecond
	maxRetries := 3

	for attempt := 0; attempt <= maxRetries; attempt++ {
		// Exponential backoff dengan jitter untuk attempt > 0
		if attempt > 0 {
			jitter := time.Duration(rand.Intn(300)) * time.Millisecond
			select {
			case <-ctx.Done():
				return nil, ctx.Err()
			case <-time.After(backoff + jitter):
			}
			backoff *= 2
		}

		req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
		if err != nil {
			return nil, err
		}

		// Rotasi User-Agent
		req.Header.Set("User-Agent", userAgents[rand.Intn(len(userAgents))])

		// Memilih klien acak (Proxy Rotasi tergabung di dalam ini)
		client := clients[rand.Intn(len(clients))]

		res, err := client.Do(req)
		if err != nil {
			lastErr = err
			continue
		}

		// Trigger retry jika Rate Limit atau Server Error
		if res.StatusCode == 429 || res.StatusCode >= 500 {
			res.Body.Close()
			lastErr = fmt.Errorf("http %d", res.StatusCode)
			continue
		}

		if res.StatusCode < 200 || res.StatusCode >= 300 {
			res.Body.Close()
			return nil, fmt.Errorf("http %d", res.StatusCode)
		}

		body, err := io.ReadAll(res.Body)
		res.Body.Close()
		if err != nil {
			lastErr = err
			continue
		}

		raw, err := decodeWindows1252(body)
		if err != nil {
			return nil, err
		}

		if strings.HasPrefix(raw, ")]}'") {
			raw = strings.TrimPrefix(raw, ")]}'")
			raw = strings.TrimPrefix(raw, "\n")
		}

		var top []any
		if err := json.Unmarshal([]byte(raw), &top); err != nil {
			lastErr = err
			continue
		}

		if len(top) == 0 {
			return nil, errors.New("unexpected response shape")
		}

		rawSuggestions, ok := top[0].([]any)
		if !ok {
			return nil, errors.New("unexpected suggestions shape")
		}

		suggestions := make([]string, 0, len(rawSuggestions))
		for _, item := range rawSuggestions {
			if row, ok := item.([]any); ok && len(row) > 0 {
				if htmlStr, ok := row[0].(string); ok {
					clean := cleanSuggestion(htmlStr)
					if clean != "" {
						suggestions = append(suggestions, clean)
					}
				}
			}
		}
		return suggestions, nil
	}

	return nil, fmt.Errorf("failed after %d retries, last error: %v", maxRetries, lastErr)
}

func decodeWindows1252(b []byte) (string, error) {
	decoded, err := charmap.Windows1252.NewDecoder().Bytes(b)
	if err != nil {
		return "", err
	}
	return string(decoded), nil
}

func cleanSuggestion(s string) string {
	s = strings.ReplaceAll(s, "<b>", "")
	s = strings.ReplaceAll(s, "</b>", "")
	s = html.UnescapeString(s)
	var b strings.Builder
	for _, r := range s {
		if r >= 0x20 && r <= 0x7E || r >= 0x00C0 && r <= 0x00FF || (r >= 0x4E00 && r <= 0x9FFF) || (r >= 0x0600 && r <= 0x06FF) || (r >= 0x0900 && r <= 0x097F) {
			b.WriteRune(r)
		}
	}
	return b.String()
}

func initialQueries(seed string, includeBase bool, alphaSuffix bool, digitSuffix bool) []string {
	seed = strings.TrimSpace(seed)
	if seed == "" {
		return nil
	}
	qs := make([]string, 0, 64)
	if includeBase {
		qs = append(qs, seed)
	}
	if alphaSuffix {
		for c := 'a'; c <= 'z'; c++ {
			qs = append(qs, seed+" "+string(c))
		}
	}
	if digitSuffix {
		for c := '0'; c <= '9'; c++ {
			qs = append(qs, seed+" "+string(c))
		}
	}
	if len(qs) == 0 {
		qs = append(qs, seed)
	}
	return qs
}

func splitKeywords(line string) []string {
	line = strings.TrimSpace(line)
	if line == "" {
		return nil
	}
	parts := strings.FieldsFunc(line, func(r rune) bool {
		return r == ',' || r == ';' || r == '|' || r == '\t'
	})
	for i := range parts {
		parts[i] = strings.TrimSpace(parts[i])
	}
	return parts
}

func loadConfig(path string) (appConfig, error) {
	if path = strings.TrimSpace(path); path == "" {
		return appConfig{}, nil
	}
	b, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return appConfig{}, nil
		}
		return appConfig{}, err
	}
	var cfg appConfig
	return cfg, json.Unmarshal(b, &cfg)
}

func findArgValue(name, def string) string {
	args := os.Args[1:]
	for i := 0; i < len(args); i++ {
		a := args[i]
		if a == name && i+1 < len(args) {
			return args[i+1]
		}
		if strings.HasPrefix(a, name+"=") {
			return strings.TrimPrefix(a, name+"=")
		}
	}
	return def
}

func parseHTTPProxy(s string) (*url.URL, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return nil, errors.New("proxy is empty")
	}
	if strings.Contains(s, "://") {
		return url.Parse(s)
	}
	if idx := strings.LastIndex(s, "@"); idx > 0 {
		auth, hostPort := s[:idx], s[idx+1:]
		hp := strings.SplitN(hostPort, ":", 2)
		user, pass, _ := strings.Cut(auth, ":")
		u := &url.URL{Scheme: "http", Host: hp[0] + ":" + hp[1]}
		u.User = url.UserPassword(user, pass)
		return u, nil
	}
	parts := strings.Split(s, ":")
	if len(parts) == 2 {
		return &url.URL{Scheme: "http", Host: parts[0] + ":" + parts[1]}, nil
	}
	if len(parts) >= 4 {
		u := &url.URL{Scheme: "http", Host: parts[0] + ":" + parts[1]}
		u.User = url.UserPassword(parts[2], strings.Join(parts[3:], ":"))
		return u, nil
	}
	return nil, errors.New("invalid proxy format")
}

func readNonEmptyLines(path string) ([]string, error) {
	if path = strings.TrimSpace(path); path == "" {
		return nil, nil
	}
	f, err := os.Open(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return nil, nil
		}
		return nil, err
	}
	defer f.Close()
	var lines []string
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		if line := strings.TrimSpace(scanner.Text()); line != "" {
			lines = append(lines, line)
		}
	}
	return lines, scanner.Err()
}