package storage

import (
	"encoding/json"
	"path"

	"github.com/Sirupsen/logrus"
	"github.com/docker/distribution/digest"
	"github.com/docker/distribution/manifest"
	"github.com/docker/libtrust"
)

// revisionStore supports storing and managing manifest revisions.
type revisionStore struct {
	*repository
}

// exists returns true if the revision is available in the named repository.
func (rs *revisionStore) exists(revision digest.Digest) (bool, error) {
	revpath, err := rs.pm.path(manifestRevisionPathSpec{
		name:     rs.Name(),
		revision: revision,
	})

	if err != nil {
		return false, err
	}

	exists, err := exists(rs.driver, revpath)
	if err != nil {
		return false, err
	}

	return exists, nil
}

// get retrieves the manifest, keyed by revision digest.
func (rs *revisionStore) get(revision digest.Digest) (*manifest.SignedManifest, error) {
	// Ensure that this revision is available in this repository.
	if exists, err := rs.exists(revision); err != nil {
		return nil, err
	} else if !exists {
		return nil, ErrUnknownManifestRevision{
			Name:     rs.Name(),
			Revision: revision,
		}
	}

	content, err := rs.blobStore.get(revision)
	if err != nil {
		return nil, err
	}

	// Fetch the signatures for the manifest
	signatures, err := rs.getSignatures(revision)
	if err != nil {
		return nil, err
	}

	jsig, err := libtrust.NewJSONSignature(content, signatures...)
	if err != nil {
		return nil, err
	}

	// Extract the pretty JWS
	raw, err := jsig.PrettySignature("signatures")
	if err != nil {
		return nil, err
	}

	var sm manifest.SignedManifest
	if err := json.Unmarshal(raw, &sm); err != nil {
		return nil, err
	}

	return &sm, nil
}

// put stores the manifest in the repository, if not already present. Any
// updated signatures will be stored, as well.
func (rs *revisionStore) put(sm *manifest.SignedManifest) (digest.Digest, error) {
	// Resolve the payload in the manifest.
	payload, err := sm.Payload()
	if err != nil {
		return "", err
	}

	// Digest and store the manifest payload in the blob store.
	revision, err := rs.blobStore.put(payload)
	if err != nil {
		logrus.Errorf("error putting payload into blobstore: %v", err)
		return "", err
	}

	// Link the revision into the repository.
	if err := rs.link(revision); err != nil {
		return "", err
	}

	// Grab each json signature and store them.
	signatures, err := sm.Signatures()
	if err != nil {
		return "", err
	}

	for _, signature := range signatures {
		if err := rs.putSignature(revision, signature); err != nil {
			return "", err
		}
	}

	return revision, nil
}

// link links the revision into the repository.
func (rs *revisionStore) link(revision digest.Digest) error {
	revisionPath, err := rs.pm.path(manifestRevisionLinkPathSpec{
		name:     rs.Name(),
		revision: revision,
	})

	if err != nil {
		return err
	}

	if exists, err := exists(rs.driver, revisionPath); err != nil {
		return err
	} else if exists {
		// Revision has already been linked!
		return nil
	}

	return rs.blobStore.link(revisionPath, revision)
}

// delete removes the specified manifest revision from storage.
func (rs *revisionStore) delete(revision digest.Digest) error {
	revisionPath, err := rs.pm.path(manifestRevisionPathSpec{
		name:     rs.Name(),
		revision: revision,
	})

	if err != nil {
		return err
	}

	return rs.driver.Delete(revisionPath)
}

// getSignatures retrieves all of the signature blobs for the specified
// manifest revision.
func (rs *revisionStore) getSignatures(revision digest.Digest) ([][]byte, error) {
	signaturesPath, err := rs.pm.path(manifestSignaturesPathSpec{
		name:     rs.Name(),
		revision: revision,
	})

	if err != nil {
		return nil, err
	}

	// Need to append signature digest algorithm to path to get all items.
	// Perhaps, this should be in the pathMapper but it feels awkward. This
	// can be eliminated by implementing listAll on drivers.
	signaturesPath = path.Join(signaturesPath, "sha256")

	signaturePaths, err := rs.driver.List(signaturesPath)
	if err != nil {
		return nil, err
	}

	var signatures [][]byte
	for _, sigPath := range signaturePaths {
		// Append the link portion
		sigPath = path.Join(sigPath, "link")

		// TODO(stevvooe): These fetches should be parallelized for performance.
		p, err := rs.blobStore.linked(sigPath)
		if err != nil {
			return nil, err
		}

		signatures = append(signatures, p)
	}

	return signatures, nil
}

// putSignature stores the signature for the provided manifest revision.
func (rs *revisionStore) putSignature(revision digest.Digest, signature []byte) error {
	signatureDigest, err := rs.blobStore.put(signature)
	if err != nil {
		return err
	}

	signaturePath, err := rs.pm.path(manifestSignatureLinkPathSpec{
		name:      rs.Name(),
		revision:  revision,
		signature: signatureDigest,
	})

	if err != nil {
		return err
	}

	return rs.blobStore.link(signaturePath, signatureDigest)
}