package docker

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/dotcloud/docker/auth"
	"github.com/shin-/cookiejar"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"path"
	"strings"
)

//FIXME: Set the endpoint in a conf file or via commandline
const INDEX_ENDPOINT = auth.INDEX_SERVER + "/v1"

// Build an Image object from raw json data
func NewImgJson(src []byte) (*Image, error) {
	ret := &Image{}

	Debugf("Json string: {%s}\n", src)
	// FIXME: Is there a cleaner way to "purify" the input json?
	if err := json.Unmarshal(src, ret); err != nil {
		return nil, err
	}
	return ret, nil
}

func doWithCookies(c *http.Client, req *http.Request) (*http.Response, error) {
	for _, cookie := range c.Jar.Cookies(req.URL) {
		req.AddCookie(cookie)
	}
	return c.Do(req)
}

// Retrieve the history of a given image from the Registry.
// Return a list of the parent's json (requested image included)
func (graph *Graph) getRemoteHistory(imgId, registry string, token []string) ([]string, error) {
	client := graph.getHttpClient()

	req, err := http.NewRequest("GET", registry+"/images/"+imgId+"/ancestry", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
	res, err := client.Do(req)
	if err != nil || res.StatusCode != 200 {
		if res != nil {
			return nil, fmt.Errorf("Internal server error: %d trying to fetch remote history for %s", res.StatusCode, imgId)
		}
		return nil, err
	}
	defer res.Body.Close()

	jsonString, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, fmt.Errorf("Error while reading the http response: %s\n", err)
	}

	Debugf("Ancestry: %s", jsonString)
	history := new([]string)
	if err := json.Unmarshal(jsonString, history); err != nil {
		return nil, err
	}
	return *history, nil
}

func (graph *Graph) getHttpClient() *http.Client {
	if graph.httpClient == nil {
		graph.httpClient = &http.Client{}
		graph.httpClient.Jar = cookiejar.NewCookieJar()
	}
	return graph.httpClient
}

// Check if an image exists in the Registry
func (graph *Graph) LookupRemoteImage(imgId, registry string, authConfig *auth.AuthConfig) bool {
	rt := &http.Transport{Proxy: http.ProxyFromEnvironment}

	req, err := http.NewRequest("GET", registry+"/images/"+imgId+"/json", nil)
	if err != nil {
		return false
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	res, err := rt.RoundTrip(req)
	return err == nil && res.StatusCode == 307
}

func (graph *Graph) getImagesInRepository(repository string, authConfig *auth.AuthConfig) ([]map[string]string, error) {
	u := INDEX_ENDPOINT + "/repositories/" + repository + "/images"
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return nil, err
	}
	if authConfig != nil && len(authConfig.Username) > 0 {
		req.SetBasicAuth(authConfig.Username, authConfig.Password)
	}
	res, err := graph.getHttpClient().Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	// Repository doesn't exist yet
	if res.StatusCode == 404 {
		return nil, nil
	}

	jsonData, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}

	imageList := []map[string]string{}

	err = json.Unmarshal(jsonData, &imageList)
	if err != nil {
		Debugf("Body: %s (%s)\n", res.Body, u)
		return nil, err
	}

	return imageList, nil
}

// Retrieve an image from the Registry.
// Returns the Image object as well as the layer as an Archive (io.Reader)
func (graph *Graph) getRemoteImage(stdout io.Writer, imgId, registry string, token []string) (*Image, Archive, error) {
	client := graph.getHttpClient()

	fmt.Fprintf(stdout, "Pulling %s metadata\r\n", imgId)
	// Get the Json
	req, err := http.NewRequest("GET", registry+"/images/"+imgId+"/json", nil)
	if err != nil {
		return nil, nil, fmt.Errorf("Failed to download json: %s", err)
	}
	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
	res, err := client.Do(req)
	if err != nil {
		return nil, nil, fmt.Errorf("Failed to download json: %s", err)
	}
	if res.StatusCode != 200 {
		return nil, nil, fmt.Errorf("HTTP code %d", res.StatusCode)
	}
	defer res.Body.Close()

	jsonString, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, nil, fmt.Errorf("Failed to download json: %s", err)
	}

	img, err := NewImgJson(jsonString)
	if err != nil {
		return nil, nil, fmt.Errorf("Failed to parse json: %s", err)
	}
	img.Id = imgId

	// Get the layer
	fmt.Fprintf(stdout, "Pulling %s fs layer\r\n", imgId)
	req, err = http.NewRequest("GET", registry+"/images/"+imgId+"/layer", nil)
	if err != nil {
		return nil, nil, fmt.Errorf("Error while getting from the server: %s\n", err)
	}
	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
	res, err = client.Do(req)
	if err != nil {
		return nil, nil, err
	}
	return img, ProgressReader(res.Body, int(res.ContentLength), stdout, "Downloading %v/%v (%v)"), nil
}

func (graph *Graph) getRemoteTags(stdout io.Writer, registries []string, repository string, token []string) (map[string]string, error) {
	client := graph.getHttpClient()
	if strings.Count(repository, "/") == 0 {
		// This will be removed once the Registry supports auto-resolution on
		// the "library" namespace
		repository = "library/" + repository
	}
	for _, host := range registries {
		endpoint := fmt.Sprintf("https://%s/v1/repositories/%s/tags", host, repository)
		req, err := http.NewRequest("GET", endpoint, nil)
		if err != nil {
			return nil, err
		}
		req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
		res, err := client.Do(req)
		defer res.Body.Close()
		Debugf("Got status code %d from %s", res.StatusCode, endpoint)
		if err != nil || (res.StatusCode != 200 && res.StatusCode != 404) {
			continue
		} else if res.StatusCode == 404 {
			return nil, fmt.Errorf("Repository not found")
		}

		result := make(map[string]string)

		rawJson, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return nil, err
		}
		if err = json.Unmarshal(rawJson, &result); err != nil {
			return nil, err
		}
		return result, nil
	}
	return nil, fmt.Errorf("Could not reach any registry endpoint")
}

func (graph *Graph) getImageForTag(stdout io.Writer, tag, remote, registry string, token []string) (string, error) {
	client := graph.getHttpClient()

	if !strings.Contains(remote, "/") {
		remote = "library/" + remote
	}

	registryEndpoint := "https://" + registry + "/v1"
	repositoryTarget := registryEndpoint + "/repositories/" + remote + "/tags/" + tag

	req, err := http.NewRequest("GET", repositoryTarget, nil)
	if err != nil {
		return "", err
	}
	req.Header.Set("Authorization", "Token "+strings.Join(token, ", "))
	res, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("Error while retrieving repository info: %v", err)
	}
	defer res.Body.Close()
	if res.StatusCode == 403 {
		return "", fmt.Errorf("You aren't authorized to access this resource")
	} else if res.StatusCode != 200 {
		return "", fmt.Errorf("HTTP code: %d", res.StatusCode)
	}

	var imgId string
	rawJson, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return "", err
	}
	if err = json.Unmarshal(rawJson, &imgId); err != nil {
		return "", err
	}
	return imgId, nil
}

func (graph *Graph) PullImage(stdout io.Writer, imgId, registry string, token []string) error {
	history, err := graph.getRemoteHistory(imgId, registry, token)
	if err != nil {
		return err
	}
	// FIXME: Try to stream the images?
	// FIXME: Launch the getRemoteImage() in goroutines
	for _, id := range history {
		if !graph.Exists(id) {
			img, layer, err := graph.getRemoteImage(stdout, id, registry, token)
			if err != nil {
				// FIXME: Keep goging in case of error?
				return err
			}
			if err = graph.Register(layer, img); err != nil {
				return err
			}
		}
	}
	return nil
}

func (graph *Graph) PullRepository(stdout io.Writer, remote, askedTag string, repositories *TagStore, authConfig *auth.AuthConfig) error {
	client := graph.getHttpClient()

	fmt.Fprintf(stdout, "Pulling repository %s from %s\r\n", remote, INDEX_ENDPOINT)
	repositoryTarget := INDEX_ENDPOINT + "/repositories/" + remote + "/images"

	req, err := http.NewRequest("GET", repositoryTarget, nil)
	if err != nil {
		return err
	}
	if authConfig != nil && len(authConfig.Username) > 0 {
		req.SetBasicAuth(authConfig.Username, authConfig.Password)
	}
	req.Header.Set("X-Docker-Token", "true")

	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode == 401 {
		return fmt.Errorf("Please login first (HTTP code %d)", res.StatusCode)
	}
	// TODO: Right now we're ignoring checksums in the response body.
	// In the future, we need to use them to check image validity.
	if res.StatusCode != 200 {
		return fmt.Errorf("HTTP code: %d", res.StatusCode)
	}

	var token, endpoints []string
	if res.Header.Get("X-Docker-Token") != "" {
		token = res.Header["X-Docker-Token"]
	}
	if res.Header.Get("X-Docker-Endpoints") != "" {
		endpoints = res.Header["X-Docker-Endpoints"]
	} else {
		return fmt.Errorf("Index response didn't contain any endpoints")
	}

	checksumsJson, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return err
	}

	// Reload the json file to make sure not to overwrite faster sums
	err = func() error {
		localChecksums := make(map[string]string)
		remoteChecksums := []struct {
			Id       string `json: "id"`
			Checksum string `json: "checksum"`
		}{}
		checksumDictPth := path.Join(graph.Root, "..", "checksums")

		if err := json.Unmarshal(checksumsJson, &remoteChecksums); err != nil {
			return err
		}

		graph.lockSumFile.Lock()
		defer graph.lockSumFile.Unlock()

		if checksumDict, err := ioutil.ReadFile(checksumDictPth); err == nil {
			if err := json.Unmarshal(checksumDict, &localChecksums); err != nil {
				return err
			}
		}

		for _, elem := range remoteChecksums {
			localChecksums[elem.Id] = elem.Checksum
		}

		checksumsJson, err = json.Marshal(localChecksums)
		if err != nil {
			return err
		}
		if err := ioutil.WriteFile(checksumDictPth, checksumsJson, 0600); err != nil {
			return err
		}
		return nil
	}()
	if err != nil {
		return err
	}

	var tagsList map[string]string
	if askedTag == "" {
		tagsList, err = graph.getRemoteTags(stdout, endpoints, remote, token)
		if err != nil {
			return err
		}
	} else {
		tagsList = map[string]string{askedTag: ""}
	}

	for askedTag, imgId := range tagsList {
		fmt.Fprintf(stdout, "Resolving tag \"%s:%s\" from %s\n", remote, askedTag, endpoints)
		success := false
		for _, registry := range endpoints {
			if imgId == "" {
				imgId, err = graph.getImageForTag(stdout, askedTag, remote, registry, token)
				if err != nil {
					fmt.Fprintf(stdout, "Error while retrieving image for tag: %v (%v) ; "+
						"checking next endpoint", askedTag, err)
					continue
				}
			}

			if err := graph.PullImage(stdout, imgId, "https://"+registry+"/v1", token); err != nil {
				return err
			}

			if err = repositories.Set(remote, askedTag, imgId, true); err != nil {
				return err
			}
			success = true
		}

		if !success {
			return fmt.Errorf("Could not find repository on any of the indexed registries.")
		}
	}

	if err = repositories.Save(); err != nil {
		return err
	}

	return nil
}

func pushImageRec(graph *Graph, stdout io.Writer, img *Image, registry string, token []string) error {
	if parent, err := img.GetParent(); err != nil {
		return err
	} else if parent != nil {
		if err := pushImageRec(graph, stdout, parent, registry, token); err != nil {
			return err
		}
	}
	client := graph.getHttpClient()
	jsonRaw, err := ioutil.ReadFile(path.Join(graph.Root, img.Id, "json"))
	if err != nil {
		return fmt.Errorf("Error while retreiving the path for {%s}: %s", img.Id, err)
	}

	fmt.Fprintf(stdout, "Pushing %s metadata\r\n", img.Id)

	// FIXME: try json with UTF8
	jsonData := strings.NewReader(string(jsonRaw))
	req, err := http.NewRequest("PUT", registry+"/images/"+img.Id+"/json", jsonData)
	if err != nil {
		return err
	}
	req.Header.Add("Content-type", "application/json")
	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))

	checksum, err := img.Checksum()
	if err != nil {
		return fmt.Errorf("Error while retrieving checksum for %s: %v", img.Id, err)
	}
	req.Header.Set("X-Docker-Checksum", checksum)
	res, err := doWithCookies(client, req)
	if err != nil {
		return fmt.Errorf("Failed to upload metadata: %s", err)
	}
	defer res.Body.Close()
	if len(res.Cookies()) > 0 {
		client.Jar.SetCookies(req.URL, res.Cookies())
	}
	if res.StatusCode != 200 {
		errBody, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return fmt.Errorf("HTTP code %d while uploading metadata and error when"+
				" trying to parse response body: %v", res.StatusCode, err)
		}
		var jsonBody map[string]string
		if err := json.Unmarshal(errBody, &jsonBody); err != nil {
			errBody = []byte(err.Error())
		} else if jsonBody["error"] == "Image already exists" {
			fmt.Fprintf(stdout, "Image %v already uploaded ; skipping\n", img.Id)
			return nil
		}
		return fmt.Errorf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody)
	}

	fmt.Fprintf(stdout, "Pushing %s fs layer\r\n", img.Id)

	layerData, err := graph.TempLayerArchive(img.Id, Xz, stdout)
	if err != nil {
		return fmt.Errorf("Failed to generate layer archive: %s", err)
	}

	req3, err := http.NewRequest("PUT", registry+"/images/"+img.Id+"/layer",
		ProgressReader(layerData, -1, stdout, ""))
	if err != nil {
		return err
	}

	req3.ContentLength = -1
	req3.TransferEncoding = []string{"chunked"}
	req3.Header.Set("Authorization", "Token "+strings.Join(token, ","))
	res3, err := doWithCookies(client, req3)
	if err != nil {
		return fmt.Errorf("Failed to upload layer: %s", err)
	}
	defer res3.Body.Close()

	if res3.StatusCode != 200 {
		errBody, err := ioutil.ReadAll(res3.Body)
		if err != nil {
			return fmt.Errorf("HTTP code %d while uploading metadata and error when"+
				" trying to parse response body: %v", res.StatusCode, err)
		}
		return fmt.Errorf("Received HTTP code %d while uploading layer: %s", res3.StatusCode, errBody)
	}
	return nil
}

// Push a local image to the registry with its history if needed
func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, registry string, token []string) error {
	registry = "https://" + registry + "/v1"
	return pushImageRec(graph, stdout, imgOrig, registry, token)
}

// push a tag on the registry.
// Remote has the format '<user>/<repo>
func (graph *Graph) pushTag(remote, revision, tag, registry string, token []string) error {
	// "jsonify" the string
	revision = "\"" + revision + "\""
	registry = "https://" + registry + "/v1"

	Debugf("Pushing tags for rev [%s] on {%s}\n", revision, registry+"/users/"+remote+"/"+tag)

	client := graph.getHttpClient()
	req, err := http.NewRequest("PUT", registry+"/repositories/"+remote+"/tags/"+tag, strings.NewReader(revision))
	if err != nil {
		return err
	}
	req.Header.Add("Content-type", "application/json")
	req.Header.Set("Authorization", "Token "+strings.Join(token, ","))
	req.ContentLength = int64(len(revision))
	res, err := doWithCookies(client, req)
	if err != nil {
		return err
	}
	res.Body.Close()
	if res.StatusCode != 200 && res.StatusCode != 201 {
		return fmt.Errorf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote)
	}
	return nil
}

// FIXME: this should really be PushTag
func (graph *Graph) pushPrimitive(stdout io.Writer, remote, tag, imgId, registry string, token []string) error {
	// Check if the local impage exists
	img, err := graph.Get(imgId)
	if err != nil {
		fmt.Fprintf(stdout, "Skipping tag %s:%s: %s does not exist\r\n", remote, tag, imgId)
		return nil
	}
	fmt.Fprintf(stdout, "Pushing image %s:%s\r\n", remote, tag)
	// Push the image
	if err = graph.PushImage(stdout, img, registry, token); err != nil {
		return err
	}
	fmt.Fprintf(stdout, "Registering tag %s:%s\r\n", remote, tag)
	// And then the tag
	if err = graph.pushTag(remote, imgId, tag, registry, token); err != nil {
		return err
	}
	return nil
}

// Push a repository to the registry.
// Remote has the format '<user>/<repo>
func (graph *Graph) PushRepository(stdout io.Writer, remote string, localRepo Repository, authConfig *auth.AuthConfig) error {
	client := graph.getHttpClient()

	checksums, err := graph.Checksums(stdout, localRepo)
	if err != nil {
		return err
	}

	imgList := make([]map[string]string, len(checksums))
	checksums2 := make([]map[string]string, len(checksums))

	uploadedImages, err := graph.getImagesInRepository(remote, authConfig)
	if err != nil {
		return fmt.Errorf("Error occured while fetching the list: %s", err)
	}

	// Filter list to only send images/checksums not already uploaded
	i := 0
	for _, obj := range checksums {
		found := false
		for _, uploadedImg := range uploadedImages {
			if obj["id"] == uploadedImg["id"] && uploadedImg["checksum"] != "" {
				found = true
				break
			}
		}
		if !found {
			imgList[i] = map[string]string{"id": obj["id"]}
			checksums2[i] = obj
			i += 1
		}
	}
	checksums = checksums2[:i]
	imgList = imgList[:i]

	imgListJson, err := json.Marshal(imgList)
	if err != nil {
		return err
	}

	req, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/", bytes.NewReader(imgListJson))
	if err != nil {
		return err
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	req.ContentLength = int64(len(imgListJson))
	req.Header.Set("X-Docker-Token", "true")
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	for res.StatusCode >= 300 && res.StatusCode < 400 {
		Debugf("Redirected to %s\n", res.Header.Get("Location"))
		req, err = http.NewRequest("PUT", res.Header.Get("Location"), bytes.NewReader(imgListJson))
		if err != nil {
			return err
		}
		req.SetBasicAuth(authConfig.Username, authConfig.Password)
		req.ContentLength = int64(len(imgListJson))
		req.Header.Set("X-Docker-Token", "true")
		res, err = client.Do(req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
	}

	if res.StatusCode != 200 && res.StatusCode != 201 {
		return fmt.Errorf("Error: Status %d trying to push repository %s", res.StatusCode, remote)
	}

	var token, endpoints []string
	if res.Header.Get("X-Docker-Token") != "" {
		token = res.Header["X-Docker-Token"]
		Debugf("Auth token: %v", token)
	} else {
		return fmt.Errorf("Index response didn't contain an access token")
	}
	if res.Header.Get("X-Docker-Endpoints") != "" {
		endpoints = res.Header["X-Docker-Endpoints"]
	} else {
		return fmt.Errorf("Index response didn't contain any endpoints")
	}

	for _, registry := range endpoints {
		fmt.Fprintf(stdout, "Pushing repository %s to %s (%d tags)\r\n", remote, registry,
			len(localRepo))
		// For each image within the repo, push them
		for tag, imgId := range localRepo {
			if err := graph.pushPrimitive(stdout, remote, tag, imgId, registry, token); err != nil {
				// FIXME: Continue on error?
				return err
			}
		}
	}
	checksumsJson, err := json.Marshal(checksums)
	if err != nil {
		return err
	}

	req2, err := http.NewRequest("PUT", INDEX_ENDPOINT+"/repositories/"+remote+"/images", bytes.NewReader(checksumsJson))
	if err != nil {
		return err
	}
	req2.SetBasicAuth(authConfig.Username, authConfig.Password)
	req2.Header["X-Docker-Endpoints"] = endpoints
	req2.ContentLength = int64(len(checksumsJson))
	res2, err := client.Do(req2)
	if err != nil {
		return err
	}
	res2.Body.Close()
	if res2.StatusCode != 204 {
		return fmt.Errorf("Error: Status %d trying to push checksums %s", res.StatusCode, remote)
	}

	return nil
}

func (graph *Graph) Checksums(output io.Writer, repo Repository) ([]map[string]string, error) {
	checksums := make(map[string]string)
	for _, id := range repo {
		img, err := graph.Get(id)
		if err != nil {
			return nil, err
		}
		err = img.WalkHistory(func(image *Image) error {
			fmt.Fprintf(output, "Computing checksum for image %s\n", image.Id)
			if _, exists := checksums[image.Id]; !exists {
				checksums[image.Id], err = image.Checksum()
				if err != nil {
					return err
				}
			}
			return nil
		})
		if err != nil {
			return nil, err
		}
	}
	i := 0
	result := make([]map[string]string, len(checksums))
	for id, sum := range checksums {
		result[i] = map[string]string{
			"id":       id,
			"checksum": sum,
		}
		i++
	}
	return result, nil
}

type SearchResults struct {
	Query      string              `json:"query"`
	NumResults int                 `json:"num_results"`
	Results    []map[string]string `json:"results"`
}

func (graph *Graph) SearchRepositories(stdout io.Writer, term string) (*SearchResults, error) {
	client := graph.getHttpClient()
	u := INDEX_ENDPOINT + "/search?q=" + url.QueryEscape(term)
	req, err := http.NewRequest("GET", u, nil)
	if err != nil {
		return nil, err
	}
	res, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()
	if res.StatusCode != 200 {
		return nil, fmt.Errorf("Unexepected status code %d", res.StatusCode)
	}
	rawData, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err
	}
	result := new(SearchResults)
	err = json.Unmarshal(rawData, result)
	return result, err
}