// Copyright 2017 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package uuid

import (
	"encoding/hex"
	"fmt"
	"io"
	"strings"

	"github.com/google/uuid"
	"github.com/open-policy-agent/opa/v1/util"
)

const BILLION = 1000000000

// New Create a version 4 random UUID
func New(r io.Reader) (string, error) {
	var arr [52]byte     // arr, same buffer for both src (16) and dst (36)
	src := arr[:16]      // src, bytes to encode
	dst := arr[16:16:52] // dst, to encode, len 0, cap 36 (for appending)

	n, err := io.ReadFull(r, src)
	if n != 16 || err != nil {
		return "", err
	}
	src[8] = src[8]&^0xc0 | 0x80
	src[6] = src[6]&^0xf0 | 0x40

	dst = append(hex.AppendEncode(dst, src[:4]), '-')
	dst = append(hex.AppendEncode(dst, src[4:6]), '-')
	dst = append(hex.AppendEncode(dst, src[6:8]), '-')
	dst = append(hex.AppendEncode(dst, src[8:10]), '-')

	return util.ByteSliceToString(hex.AppendEncode(dst, src[10:])), nil
}

// Parse will use the google/uuid library to parse the string into a uuid
// if parsing fails, it will return an empty map. It will fill the map
// with some decoded values with fillMap
// ref: https://datatracker.ietf.org/doc/html/rfc4122
func Parse(s string) (map[string]any, error) {
	uuid, err := uuid.Parse(s)
	if err != nil {
		return nil, err
	}
	out := make(map[string]any, getVersionLen(int(uuid.Version())))
	fillMap(out, uuid)
	return out, nil
}

// Fills the map with values from the uuid. Version and variant for every version.
// Version 1-2 has decodable values that could be of use, version 4 is random,
// and version 3,5 is not feasible to extract data. Generated with either MD5 or SHA1 hash
// ref: https://datatracker.ietf.org/doc/html/rfc4122 about creation of UUIDs
func fillMap(m map[string]any, u uuid.UUID) {
	m["version"] = int(u.Version())
	m["variant"] = u.Variant().String()
	switch version := m["version"]; version {
	case 1, 2:
		m["time"] = nanoUnix(u.Time())
		m["nodeid"] = byteDecimalToHexMAC(u.NodeID(), "-")
		m["macvariables"] = macVars(u.NodeID()[0])
		m["clocksequence"] = u.ClockSequence()
		if version == 2 {
			m["id"] = int(u.ID())
			m["domain"] = u.Domain().String()
		}
	}
}

// macVars will take the first byte of a MAC-address and check for the
// local/global bit and check for the unicast/multicast bit of the byte,
// and return a string with this info.
// ref: https://datatracker.ietf.org/doc/html/rfc7042#section-2.1
func macVars(inpb byte) string {
	switch {
	case inpb&byte(0b11) == byte(0b11):
		return "local:multicast"
	case inpb&byte(0b01) == byte(0b01):
		return "global:multicast"
	case inpb&byte(0b10) == byte(0b10):
		return "local:unicast"
	}
	return "global:unicast"
}

// loops through the byte array to convert all bytes to hexes.
// It will also put the separator between every other to make it human-readable
func byteDecimalToHexMAC(bytes []byte, sep string) string {
	hexs := strings.Builder{}
	l := len(bytes)
	hexs.Grow((l * 3) - 1) // 1 byte -> 2 hexes + 1 separator (if one char)

	for i, b := range bytes {
		fmt.Fprintf(&hexs, "%02x", b)
		if i < l-1 {
			hexs.WriteString(sep)
		}
	}

	return hexs.String()
}

// nanoUnix Converts the uuids encoded time into unix represented time in nanoseconds
func nanoUnix(t uuid.Time) int64 {
	unixsec, unixnsec := t.UnixTime()
	return unixsec*BILLION + unixnsec
}

// Helper function to make map with length based on version of uuid
// Most are 2 in length (version, variant), but version 1 and 2 have more.
func getVersionLen(version int) int {
	switch version {
	case 1:
		return 5
	case 2:
		return 7
	default:
		return 2
	}
}
