package docker

import (
	"encoding/json"
	"fmt"
	"github.com/dotcloud/docker/auth"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"path"
	"strings"
)

//FIXME: Set the endpoint in a conf file or via commandline
//const REGISTRY_ENDPOINT = "http://registry-creack.dotcloud.com/v1"
const REGISTRY_ENDPOINT = auth.REGISTRY_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
}

// Build an Image object list from a raw json data
// FIXME: Do this in "stream" mode
func NewMultipleImgJson(src []byte) ([]*Image, error) {
	ret := []*Image{}

	dec := json.NewDecoder(strings.NewReader(string(src)))
	for {
		m := &Image{}
		if err := dec.Decode(m); err == io.EOF {
			break
		} else if err != nil {
			return nil, err
		}
		ret = append(ret, m)
	}
	return ret, nil
}

// 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 string, authConfig *auth.AuthConfig) ([]*Image, error) {
	client := &http.Client{}

	req, err := http.NewRequest("GET", REGISTRY_ENDPOINT+"/images/"+imgId+"/history", nil)
	if err != nil {
		return nil, err
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	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)
	}

	history, err := NewMultipleImgJson(jsonString)
	if err != nil {
		return nil, fmt.Errorf("Error while parsing the json: %s\n", err)
	}
	return history, nil
}

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

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

// 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 string, authConfig *auth.AuthConfig) (*Image, Archive, error) {
	client := &http.Client{}

	fmt.Fprintf(stdout, "Pulling %s metadata\r\n", imgId)
	// Get the Json
	req, err := http.NewRequest("GET", REGISTRY_ENDPOINT+"/images/"+imgId+"/json", nil)
	if err != nil {
		return nil, nil, fmt.Errorf("Failed to download json: %s", err)
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	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_ENDPOINT+"/images/"+imgId+"/layer", nil)
	if err != nil {
		return nil, nil, fmt.Errorf("Error while getting from the server: %s\n", err)
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	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) PullImage(stdout io.Writer, imgId string, authConfig *auth.AuthConfig) error {
	history, err := graph.getRemoteHistory(imgId, authConfig)
	if err != nil {
		return err
	}
	// FIXME: Try to stream the images?
	// FIXME: Lunch the getRemoteImage() in goroutines
	for _, j := range history {
		if !graph.Exists(j.Id) {
			img, layer, err := graph.getRemoteImage(stdout, j.Id, authConfig)
			if err != nil {
				// FIXME: Keep goging in case of error?
				return err
			}
			if err = graph.Register(layer, img); err != nil {
				return err
			}
		}
	}
	return nil
}

// FIXME: Handle the askedTag parameter
func (graph *Graph) PullRepository(stdout io.Writer, remote, askedTag string, repositories *TagStore, authConfig *auth.AuthConfig) error {
	client := &http.Client{}

	fmt.Fprintf(stdout, "Pulling repository %s\r\n", remote)

	var repositoryTarget string
	// If we are asking for 'root' repository, lookup on the Library's registry
	if strings.Index(remote, "/") == -1 {
		repositoryTarget = REGISTRY_ENDPOINT + "/library/" + remote
	} else {
		repositoryTarget = REGISTRY_ENDPOINT + "/users/" + remote
	}

	req, err := http.NewRequest("GET", repositoryTarget, nil)
	if err != nil {
		return err
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	res, err := client.Do(req)
	if err != nil {
		return err
	}
	defer res.Body.Close()
	if res.StatusCode != 200 {
		return fmt.Errorf("HTTP code: %d", res.StatusCode)
	}
	rawJson, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return err
	}
	t := map[string]string{}
	if err = json.Unmarshal(rawJson, &t); err != nil {
		return err
	}
	for tag, rev := range t {
		fmt.Fprintf(stdout, "Pulling tag %s:%s\r\n", remote, tag)
		if err = graph.PullImage(stdout, rev, authConfig); err != nil {
			return err
		}
		if err = repositories.Set(remote, tag, rev, true); err != nil {
			return err
		}
	}
	if err = repositories.Save(); err != nil {
		return err
	}
	return nil
}

// Push a local image to the registry with its history if needed
func (graph *Graph) PushImage(stdout io.Writer, imgOrig *Image, authConfig *auth.AuthConfig) error {
	client := &http.Client{}

	// FIXME: Factorize the code
	// FIXME: Do the puts in goroutines
	if err := imgOrig.WalkHistory(func(img *Image) error {

		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_ENDPOINT+"/images/"+img.Id+"/json", jsonData)
		if err != nil {
			return err
		}
		req.Header.Add("Content-type", "application/json")
		req.SetBasicAuth(authConfig.Username, authConfig.Password)
		res, err := client.Do(req)
		if err != nil {
			return fmt.Errorf("Failed to upload metadata: %s", err)
		}
		defer res.Body.Close()
		if res.StatusCode != 200 {
			switch res.StatusCode {
			case 204:
				// Case where the image is already on the Registry
				// FIXME: Do not be silent?
				return nil
			default:
				errBody, err := ioutil.ReadAll(res.Body)
				if err != nil {
					errBody = []byte(err.Error())
				}
				return fmt.Errorf("HTTP code %d while uploading metadata: %s", res.StatusCode, errBody)
			}
		}

		fmt.Fprintf(stdout, "Pushing %s fs layer\r\n", img.Id)
		req2, err := http.NewRequest("PUT", REGISTRY_ENDPOINT+"/images/"+img.Id+"/layer", nil)
		req2.SetBasicAuth(authConfig.Username, authConfig.Password)
		res2, err := client.Do(req2)
		if err != nil {
			return fmt.Errorf("Registry returned error: %s", err)
		}
		res2.Body.Close()
		if res2.StatusCode != 307 {
			return fmt.Errorf("Registry returned unexpected HTTP status code %d, expected 307", res2.StatusCode)
		}
		url, err := res2.Location()
		if err != nil || url == nil {
			return fmt.Errorf("Failed to retrieve layer upload location: %s", err)
		}

		// FIXME: stream the archive directly to the registry instead of buffering it on disk. This requires either:
		//	a) Implementing S3's proprietary streaming logic, or
		//	b) Stream directly to the registry instead of S3.
		// I prefer option b. because it doesn't lock us into a proprietary cloud service.
		tmpLayer, err := graph.TempLayerArchive(img.Id, Xz, stdout)
		if err != nil {
			return err
		}
		defer os.Remove(tmpLayer.Name())
		req3, err := http.NewRequest("PUT", url.String(), ProgressReader(tmpLayer, int(tmpLayer.Size), stdout, "Uploading %v/%v (%v)"))
		if err != nil {
			return err
		}
		req3.ContentLength = int64(tmpLayer.Size)

		req3.TransferEncoding = []string{"none"}
		res3, err := client.Do(req3)
		if err != nil {
			return fmt.Errorf("Failed to upload layer: %s", err)
		}
		res3.Body.Close()
		if res3.StatusCode != 200 {
			return fmt.Errorf("Received HTTP code %d while uploading layer", res3.StatusCode)
		}
		return nil
	}); err != nil {
		return err
	}
	return nil
}

// push a tag on the registry.
// Remote has the format '<user>/<repo>
func (graph *Graph) pushTag(remote, revision, tag string, authConfig *auth.AuthConfig) error {

	// Keep this for backward compatibility
	if tag == "" {
		tag = "lastest"
	}

	// "jsonify" the string
	revision = "\"" + revision + "\""

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

	client := &http.Client{}
	req, err := http.NewRequest("PUT", REGISTRY_ENDPOINT+"/users/"+remote+"/"+tag, strings.NewReader(revision))
	req.Header.Add("Content-type", "application/json")
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	res, err := client.Do(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)
	}
	Debugf("Result of push tag: %d\n", res.StatusCode)
	switch res.StatusCode {
	default:
		return fmt.Errorf("Error %d\n", res.StatusCode)
	case 200:
	case 201:
	}
	return nil
}

func (graph *Graph) LookupRemoteRepository(remote string, authConfig *auth.AuthConfig) bool {
	rt := &http.Transport{Proxy: http.ProxyFromEnvironment}

	var repositoryTarget string
	// If we are asking for 'root' repository, lookup on the Library's registry
	if strings.Index(remote, "/") == -1 {
		repositoryTarget = REGISTRY_ENDPOINT + "/library/" + remote + "/lookup"
	} else {
		repositoryTarget = REGISTRY_ENDPOINT + "/users/" + remote + "/lookup"
	}
	Debugf("Checking for permissions on: %s", repositoryTarget)
	req, err := http.NewRequest("PUT", repositoryTarget, strings.NewReader("\"\""))
	if err != nil {
		Debugf("%s\n", err)
		return false
	}
	req.SetBasicAuth(authConfig.Username, authConfig.Password)
	req.Header.Add("Content-type", "application/json")
	res, err := rt.RoundTrip(req)
	if err != nil || res.StatusCode != 404 {
		errBody, err := ioutil.ReadAll(res.Body)
		if err != nil {
			errBody = []byte(err.Error())
		}
		Debugf("Lookup status code: %d (body: %s)", res.StatusCode, errBody)
		return false
	}
	return true
}

// FIXME: this should really be PushTag
func (graph *Graph) pushPrimitive(stdout io.Writer, remote, tag, imgId string, authConfig *auth.AuthConfig) 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 tag %s:%s\r\n", remote, tag)
	// Push the image
	if err = graph.PushImage(stdout, img, authConfig); 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, authConfig); 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 {
	// Check if the remote repository exists/if we have the permission
	if !graph.LookupRemoteRepository(remote, authConfig) {
		return fmt.Errorf("Permission denied on repository %s\n", remote)
	}

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