// Copyright (C) The Arvados Authors. All rights reserved.
//
// SPDX-License-Identifier: Apache-2.0

package arvados

import (
	"bytes"
	"crypto/md5"
	"fmt"
	"regexp"
	"strings"
	"time"

	"git.arvados.org/arvados.git/sdk/go/blockdigest"
)

var (
	UUIDMatch = regexp.MustCompile(`^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}$`).MatchString
	PDHMatch  = regexp.MustCompile(`^[0-9a-f]{32}\+\d+$`).MatchString
)

// Collection is an arvados#collection resource.
type Collection struct {
	UUID                      string                 `json:"uuid"`
	Etag                      string                 `json:"etag"`
	OwnerUUID                 string                 `json:"owner_uuid"`
	TrashAt                   *time.Time             `json:"trash_at"`
	ManifestText              string                 `json:"manifest_text"`
	UnsignedManifestText      string                 `json:"unsigned_manifest_text"`
	Name                      string                 `json:"name"`
	CreatedAt                 time.Time              `json:"created_at"`
	ModifiedAt                time.Time              `json:"modified_at"`
	ModifiedByUserUUID        string                 `json:"modified_by_user_uuid"`
	PortableDataHash          string                 `json:"portable_data_hash"`
	ReplicationConfirmed      *int                   `json:"replication_confirmed"`
	ReplicationConfirmedAt    *time.Time             `json:"replication_confirmed_at"`
	ReplicationDesired        *int                   `json:"replication_desired"`
	StorageClassesDesired     []string               `json:"storage_classes_desired"`
	StorageClassesConfirmed   []string               `json:"storage_classes_confirmed"`
	StorageClassesConfirmedAt *time.Time             `json:"storage_classes_confirmed_at"`
	DeleteAt                  *time.Time             `json:"delete_at"`
	IsTrashed                 bool                   `json:"is_trashed"`
	Properties                map[string]interface{} `json:"properties"`
	WritableBy                []string               `json:"writable_by,omitempty"`
	FileCount                 int                    `json:"file_count"`
	FileSizeTotal             int64                  `json:"file_size_total"`
	Version                   int                    `json:"version"`
	PreserveVersion           bool                   `json:"preserve_version"`
	CurrentVersionUUID        string                 `json:"current_version_uuid"`
	Description               string                 `json:"description"`
}

func (c Collection) resourceName() string {
	return "collection"
}

// SizedDigests returns the hash+size part of each data block
// referenced by the collection.
//
// Zero-length blocks are not included.
func (c *Collection) SizedDigests() ([]SizedDigest, error) {
	manifestText := []byte(c.ManifestText)
	if len(manifestText) == 0 {
		manifestText = []byte(c.UnsignedManifestText)
	}
	if len(manifestText) == 0 && c.PortableDataHash != "d41d8cd98f00b204e9800998ecf8427e+0" {
		// TODO: Check more subtle forms of corruption, too
		return nil, fmt.Errorf("manifest is missing")
	}
	sds := make([]SizedDigest, 0, len(manifestText)/40)
	for _, line := range bytes.Split(manifestText, []byte{'\n'}) {
		if len(line) == 0 {
			continue
		}
		tokens := bytes.Split(line, []byte{' '})
		if len(tokens) < 3 {
			return nil, fmt.Errorf("Invalid stream (<3 tokens): %q", line)
		}
		for _, token := range tokens[1:] {
			if !blockdigest.LocatorPattern.Match(token) {
				// FIXME: ensure it's a file token
				break
			}
			if bytes.HasPrefix(token, []byte("d41d8cd98f00b204e9800998ecf8427e+0")) {
				// Exclude "empty block" placeholder
				continue
			}
			// FIXME: shouldn't assume 32 char hash
			if i := bytes.IndexRune(token[33:], '+'); i >= 0 {
				token = token[:33+i]
			}
			sds = append(sds, SizedDigest(string(token)))
		}
	}
	return sds, nil
}

type CollectionList struct {
	Items          []Collection `json:"items"`
	ItemsAvailable int          `json:"items_available"`
	Offset         int          `json:"offset"`
	Limit          int          `json:"limit"`
}

// PortableDataHash computes the portable data hash of the given
// manifest.
func PortableDataHash(mt string) string {
	// To calculate the PDH, we write the manifest to an md5 hash
	// func, except we skip the "extra" part of block tokens that
	// look like "abcdef0123456789abcdef0123456789+12345+extra".
	//
	// This code is simplified by the facts that (A) all block
	// tokens -- even the first and last in a stream -- are
	// preceded and followed by a space character; and (B) all
	// non-block tokens either start with '.'  or contain ':'.
	//
	// A regexp-based approach (like the one this replaced) would
	// be more readable, but very slow.
	h := md5.New()
	size := 0
	todo := []byte(mt)
	for len(todo) > 0 {
		// sp is the end of the current token (note that if
		// the current token is the last file token in a
		// stream, we'll also include the \n and the dirname
		// token on the next line, which is perfectly fine for
		// our purposes).
		sp := bytes.IndexByte(todo, ' ')
		if sp < 0 {
			// Last token of the manifest, which is never
			// a block token.
			n, _ := h.Write(todo)
			size += n
			break
		}
		if sp >= 34 && todo[32] == '+' && bytes.IndexByte(todo[:32], ':') == -1 && todo[0] != '.' {
			// todo[:sp] is a block token.
			sizeend := bytes.IndexByte(todo[33:sp], '+')
			if sizeend < 0 {
				// "hash+size"
				sizeend = sp
			} else {
				// "hash+size+extra"
				sizeend += 33
			}
			n, _ := h.Write(todo[:sizeend])
			h.Write([]byte{' '})
			size += n + 1
		} else {
			// todo[:sp] is not a block token.
			n, _ := h.Write(todo[:sp+1])
			size += n
		}
		todo = todo[sp+1:]
	}
	return fmt.Sprintf("%x+%d", h.Sum(nil), size)
}

// CollectionIDFromDNSName returns a UUID or PDH if s begins with a
// UUID or URL-encoded PDH; otherwise "".
func CollectionIDFromDNSName(s string) string {
	// Strip domain.
	if i := strings.IndexRune(s, '.'); i >= 0 {
		s = s[:i]
	}
	// Names like {uuid}--collections.example.com serve the same
	// purpose as {uuid}.collections.example.com but can reduce
	// cost/effort of using [additional] wildcard certificates.
	if i := strings.Index(s, "--"); i >= 0 {
		s = s[:i]
	}
	if UUIDMatch(s) {
		return s
	}
	if pdh := strings.Replace(s, "-", "+", 1); PDHMatch(pdh) {
		return pdh
	}
	return ""
}
