package glance

import (
	"context"
	"fmt"
	"io"
	"net"
	"net/http"
	"runtime"
	"strings"
	"sync"
	"time"
)

const httpTestRequestTimeout = 10 * time.Second

var diagnosticSteps = []diagnosticStep{
	{
		name: "resolve cloudflare.com through Cloudflare DoH",
		fn: func() (string, error) {
			return testHttpRequestWithHeaders("GET", "https://1.1.1.1/dns-query?name=cloudflare.com", map[string]string{
				"accept": "application/dns-json",
			}, 200)
		},
	},
	{
		name: "resolve cloudflare.com through Google DoH",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://8.8.8.8/resolve?name=cloudflare.com", 200)
		},
	},
	{
		name: "resolve github.com",
		fn: func() (string, error) {
			return testDNSResolution("github.com")
		},
	},
	{
		name: "resolve reddit.com",
		fn: func() (string, error) {
			return testDNSResolution("reddit.com")
		},
	},
	{
		name: "resolve twitch.tv",
		fn: func() (string, error) {
			return testDNSResolution("twitch.tv")
		},
	},
	{
		name: "fetch data from YouTube RSS feed",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://www.youtube.com/feeds/videos.xml?channel_id=UCZU9T1ceaOgwfLRq7OKFU4Q", 200)
		},
	},
	{
		name: "fetch data from Twitch.tv GQL",
		fn: func() (string, error) {
			// this should always return 0 bytes, we're mainly looking for a 200 status code
			return testHttpRequest("OPTIONS", "https://gql.twitch.tv/gql", 200)
		},
	},
	{
		name: "fetch data from GitHub API",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://api.github.com", 200)
		},
	},
	{
		name: "fetch data from Open-Meteo API",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://geocoding-api.open-meteo.com/v1/search?name=London", 200)
		},
	},
	{
		name: "fetch data from Reddit API",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://www.reddit.com/search.json", 200)
		},
	},
	{
		name: "fetch data from Yahoo finance API",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://query1.finance.yahoo.com/v8/finance/chart/NVDA", 200)
		},
	},
	{
		name: "fetch data from Hacker News Firebase API",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://hacker-news.firebaseio.com/v0/topstories.json", 200)
		},
	},
	{
		name: "fetch data from Docker Hub API",
		fn: func() (string, error) {
			return testHttpRequest("GET", "https://hub.docker.com/v2/namespaces/library/repositories/ubuntu/tags/latest", 200)
		},
	},
}

func runDiagnostic() {
	fmt.Println("```")
	fmt.Println("Glance version: " + buildVersion)
	fmt.Println("Go version: " + runtime.Version())
	fmt.Printf("Platform: %s / %s / %d CPUs\n", runtime.GOOS, runtime.GOARCH, runtime.NumCPU())
	fmt.Println("In Docker container: " + boolToString(isRunningInsideDockerContainer(), "yes", "no"))

	fmt.Printf("\nChecking network connectivity, this may take up to %d seconds...\n\n", int(httpTestRequestTimeout.Seconds()))

	var wg sync.WaitGroup
	for i := range diagnosticSteps {
		step := &diagnosticSteps[i]
		wg.Add(1)
		go func() {
			defer wg.Done()
			start := time.Now()
			step.extraInfo, step.err = step.fn()
			step.elapsed = time.Since(start)
		}()
	}
	wg.Wait()

	for _, step := range diagnosticSteps {
		var extraInfo string

		if step.extraInfo != "" {
			extraInfo = "| " + step.extraInfo + " "
		}

		fmt.Printf(
			"%s %s %s| %dms\n",
			boolToString(step.err == nil, "✓ Can", "✗ Can't"),
			step.name,
			extraInfo,
			step.elapsed.Milliseconds(),
		)

		if step.err != nil {
			fmt.Printf("└╴ error: %v\n", step.err)
		}
	}
	fmt.Println("```")
}

type diagnosticStep struct {
	name      string
	fn        func() (string, error)
	extraInfo string
	err       error
	elapsed   time.Duration
}

func testHttpRequest(method, url string, expectedStatusCode int) (string, error) {
	return testHttpRequestWithHeaders(method, url, nil, expectedStatusCode)
}

func testHttpRequestWithHeaders(method, url string, headers map[string]string, expectedStatusCode int) (string, error) {
	ctx, cancel := context.WithTimeout(context.Background(), httpTestRequestTimeout)
	defer cancel()

	request, _ := http.NewRequestWithContext(ctx, method, url, nil)
	for key, value := range headers {
		request.Header.Add(key, value)
	}

	response, err := http.DefaultClient.Do(request)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return "", err
	}

	printableBody := strings.ReplaceAll(string(body), "\n", "")
	if len(printableBody) > 50 {
		printableBody = printableBody[:50] + "..."
	}
	if len(printableBody) > 0 {
		printableBody = ", " + printableBody
	}

	extraInfo := fmt.Sprintf("%d bytes%s", len(body), printableBody)

	if response.StatusCode != expectedStatusCode {
		return extraInfo, fmt.Errorf("expected status code %d, got %d", expectedStatusCode, response.StatusCode)
	}

	return extraInfo, nil
}

func testDNSResolution(domain string) (string, error) {
	ips, err := net.LookupIP(domain)

	var ipStrings []string
	if err == nil {
		for i := range ips {
			ipStrings = append(ipStrings, ips[i].String())
		}
	}

	return strings.Join(ipStrings, ", "), err
}
