// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package web2

import (
	"bytes"
	"cmd/go/internal/base"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"runtime/debug"
	"strings"
	"sync"
)

var TraceGET = false
var webstack = false

func init() {
	flag.BoolVar(&TraceGET, "webtrace", TraceGET, "trace GET requests")
	flag.BoolVar(&webstack, "webstack", webstack, "print stack for GET requests")
}

type netrcLine struct {
	machine  string
	login    string
	password string
}

var netrcOnce sync.Once
var netrc []netrcLine

func parseNetrc(data string) []netrcLine {
	var nrc []netrcLine
	var l netrcLine
	for _, line := range strings.Split(data, "\n") {
		f := strings.Fields(line)
		for i := 0; i < len(f)-1; i += 2 {
			switch f[i] {
			case "machine":
				l.machine = f[i+1]
			case "login":
				l.login = f[i+1]
			case "password":
				l.password = f[i+1]
			}
		}
		if l.machine != "" && l.login != "" && l.password != "" {
			nrc = append(nrc, l)
			l = netrcLine{}
		}
	}
	return nrc
}

func havePassword(machine string) bool {
	netrcOnce.Do(readNetrc)
	for _, line := range netrc {
		if line.machine == machine {
			return true
		}
	}
	return false
}

func netrcPath() string {
	switch runtime.GOOS {
	case "windows":
		return filepath.Join(os.Getenv("USERPROFILE"), "_netrc")
	case "plan9":
		return filepath.Join(os.Getenv("home"), ".netrc")
	default:
		return filepath.Join(os.Getenv("HOME"), ".netrc")
	}
}

func readNetrc() {
	data, err := ioutil.ReadFile(netrcPath())
	if err != nil {
		return
	}
	netrc = parseNetrc(string(data))
}

type getState struct {
	req      *http.Request
	resp     *http.Response
	body     io.ReadCloser
	non200ok bool
}

type Option interface {
	option(*getState) error
}

func Non200OK() Option {
	return optionFunc(func(g *getState) error {
		g.non200ok = true
		return nil
	})
}

type optionFunc func(*getState) error

func (f optionFunc) option(g *getState) error {
	return f(g)
}

func DecodeJSON(dst interface{}) Option {
	return optionFunc(func(g *getState) error {
		if g.resp != nil {
			return json.NewDecoder(g.body).Decode(dst)
		}
		return nil
	})
}

func ReadAllBody(body *[]byte) Option {
	return optionFunc(func(g *getState) error {
		if g.resp != nil {
			var err error
			*body, err = ioutil.ReadAll(g.body)
			return err
		}
		return nil
	})
}

func Body(body *io.ReadCloser) Option {
	return optionFunc(func(g *getState) error {
		if g.resp != nil {
			*body = g.body
			g.body = nil
		}
		return nil
	})
}

func Header(hdr *http.Header) Option {
	return optionFunc(func(g *getState) error {
		if g.resp != nil {
			*hdr = CopyHeader(g.resp.Header)
		}
		return nil
	})
}

func CopyHeader(hdr http.Header) http.Header {
	if hdr == nil {
		return nil
	}
	h2 := make(http.Header)
	for k, v := range hdr {
		v2 := make([]string, len(v))
		copy(v2, v)
		h2[k] = v2
	}
	return h2
}

var cache struct {
	mu    sync.Mutex
	byURL map[string]*cacheEntry
}

type cacheEntry struct {
	mu   sync.Mutex
	resp *http.Response
	body []byte
}

var httpDo = http.DefaultClient.Do

func SetHTTPDoForTesting(do func(*http.Request) (*http.Response, error)) {
	if do == nil {
		do = http.DefaultClient.Do
	}
	httpDo = do
}

func Get(url string, options ...Option) error {
	if TraceGET || webstack {
		println("GET", url)
		if webstack {
			println(string(debug.Stack()))
		}
	}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return err
	}

	netrcOnce.Do(readNetrc)
	for _, l := range netrc {
		if l.machine == req.URL.Host {
			req.SetBasicAuth(l.login, l.password)
			break
		}
	}

	g := &getState{req: req}
	for _, o := range options {
		if err := o.option(g); err != nil {
			return err
		}
	}

	cache.mu.Lock()
	e := cache.byURL[url]
	if e == nil {
		e = new(cacheEntry)
		if !strings.HasPrefix(url, "file:") {
			if cache.byURL == nil {
				cache.byURL = make(map[string]*cacheEntry)
			}
			cache.byURL[url] = e
		}
	}
	cache.mu.Unlock()

	e.mu.Lock()
	if strings.HasPrefix(url, "file:") {
		body, err := ioutil.ReadFile(req.URL.Path)
		if err != nil {
			e.mu.Unlock()
			return err
		}
		e.body = body
		e.resp = &http.Response{
			StatusCode: 200,
		}
	} else if e.resp == nil {
		resp, err := httpDo(req)
		if err != nil {
			e.mu.Unlock()
			return err
		}
		e.resp = resp
		// TODO: Spool to temp file.
		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		resp.Body = nil
		if err != nil {
			e.mu.Unlock()
			return err
		}
		e.body = body
	}
	g.resp = e.resp
	g.body = ioutil.NopCloser(bytes.NewReader(e.body))
	e.mu.Unlock()

	defer func() {
		if g.body != nil {
			g.body.Close()
		}
	}()

	if g.resp.StatusCode == 403 && req.URL.Host == "api.github.com" && !havePassword("api.github.com") {
		base.Errorf("%s", githubMessage)
	}
	if !g.non200ok && g.resp.StatusCode != 200 {
		return fmt.Errorf("unexpected status (%s): %v", url, g.resp.Status)
	}

	for _, o := range options {
		if err := o.option(g); err != nil {
			return err
		}
	}
	return err
}

var githubMessage = `go: 403 response from api.github.com

GitHub applies fairly small rate limits to unauthenticated users, and
you appear to be hitting them. To authenticate, please visit
https://github.com/settings/tokens and click "Generate New Token" to
create a Personal Access Token. The token only needs "public_repo"
scope, but you can add "repo" if you want to access private
repositories too.

Add the token to your $HOME/.netrc (%USERPROFILE%\_netrc on Windows):

    machine api.github.com login YOU password TOKEN

Sorry for the interruption.
`
