package config

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"
	"io"
	"time"

	"github.com/cockroachdb/pebble/v2"
	"github.com/ipfs/kubo/core/coreiface/options"
	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/peer"
)

func Init(out io.Writer, nBitsForKeypair int) (*Config, error) {
	identity, err := CreateIdentity(out, []options.KeyGenerateOption{options.Key.Size(nBitsForKeypair)})
	if err != nil {
		return nil, err
	}

	return InitWithIdentity(identity)
}

func InitWithIdentity(identity Identity) (*Config, error) {
	datastore := DefaultDatastoreConfig()

	conf := &Config{
		API: API{
			HTTPHeaders: map[string][]string{},
		},

		// setup the node's default addresses.
		// NOTE: two swarm listen addrs, one tcp, one utp.
		Addresses: addressesConfig(),

		Datastore: datastore,
		Bootstrap: []string{AutoPlaceholder},
		Identity:  identity,
		Discovery: Discovery{
			MDNS: MDNS{
				Enabled: true,
			},
		},

		// setup the node mount points.
		Mounts: Mounts{
			IPFS: "/ipfs",
			IPNS: "/ipns",
			MFS:  "/mfs",
		},

		Ipns: Ipns{
			ResolveCacheSize:    128,
			DelegatedPublishers: []string{AutoPlaceholder},
		},

		Gateway: Gateway{
			RootRedirect: "",
			NoFetch:      false,
			HTTPHeaders:  map[string][]string{},
		},
		Pinning: Pinning{
			RemoteServices: map[string]RemotePinningService{},
		},
		DNS: DNS{
			Resolvers: map[string]string{
				".": AutoPlaceholder,
			},
		},
		Routing: Routing{
			DelegatedRouters: []string{AutoPlaceholder},
		},
	}

	return conf, nil
}

// DefaultConnMgrHighWater is the default value for the connection managers
// 'high water' mark.
const DefaultConnMgrHighWater = 96

// DefaultConnMgrLowWater is the default value for the connection managers 'low
// water' mark.
const DefaultConnMgrLowWater = 32

// DefaultConnMgrGracePeriod is the default value for the connection managers
// grace period.
const DefaultConnMgrGracePeriod = time.Second * 20

// DefaultConnMgrSilencePeriod controls how often the connection manager enforces the limits.
const DefaultConnMgrSilencePeriod = time.Second * 10

// DefaultConnMgrType is the default value for the connection managers
// type.
const DefaultConnMgrType = "basic"

// DefaultResourceMgrMinInboundConns is a MAGIC number that probably a good
// enough number of inbound conns to be a good network citizen.
const DefaultResourceMgrMinInboundConns = 800

func addressesConfig() Addresses {
	return Addresses{
		Swarm: []string{
			"/ip4/0.0.0.0/tcp/4001",
			"/ip6/::/tcp/4001",
			"/ip4/0.0.0.0/udp/4001/webrtc-direct",
			"/ip4/0.0.0.0/udp/4001/quic-v1",
			"/ip4/0.0.0.0/udp/4001/quic-v1/webtransport",
			"/ip6/::/udp/4001/webrtc-direct",
			"/ip6/::/udp/4001/quic-v1",
			"/ip6/::/udp/4001/quic-v1/webtransport",
		},
		Announce:       []string{},
		AppendAnnounce: []string{},
		NoAnnounce:     []string{},
		API:            Strings{"/ip4/127.0.0.1/tcp/5001"},
		Gateway:        Strings{"/ip4/127.0.0.1/tcp/8080"},
	}
}

// DefaultDatastoreConfig is an internal function exported to aid in testing.
func DefaultDatastoreConfig() Datastore {
	return Datastore{
		StorageMax:         "10GB",
		StorageGCWatermark: 90, // 90%
		GCPeriod:           "1h",
		BloomFilterSize:    0,
		Spec:               flatfsSpec(),
	}
}

func pebbleSpec() map[string]interface{} {
	return map[string]interface{}{
		"type":               "pebbleds",
		"prefix":             "pebble.datastore",
		"path":               "pebbleds",
		"formatMajorVersion": int(pebble.FormatNewest),
	}
}

func pebbleSpecMeasure() map[string]interface{} {
	return map[string]interface{}{
		"type":   "measure",
		"prefix": "pebble.datastore",
		"child": map[string]interface{}{
			"formatMajorVersion": int(pebble.FormatNewest),
			"type":               "pebbleds",
			"path":               "pebbleds",
		},
	}
}

func badgerSpec() map[string]interface{} {
	return map[string]interface{}{
		"type":       "badgerds",
		"prefix":     "badger.datastore",
		"path":       "badgerds",
		"syncWrites": false,
		"truncate":   true,
	}
}

func badgerSpecMeasure() map[string]interface{} {
	return map[string]interface{}{
		"type":   "measure",
		"prefix": "badger.datastore",
		"child": map[string]interface{}{
			"type":       "badgerds",
			"path":       "badgerds",
			"syncWrites": false,
			"truncate":   true,
		},
	}
}

func flatfsSpec() map[string]interface{} {
	return map[string]interface{}{
		"type": "mount",
		"mounts": []interface{}{
			map[string]interface{}{
				"mountpoint": "/blocks",
				"type":       "flatfs",
				"prefix":     "flatfs.datastore",
				"path":       "blocks",
				"sync":       false,
				"shardFunc":  "/repo/flatfs/shard/v1/next-to-last/2",
			},
			map[string]interface{}{
				"mountpoint":  "/",
				"type":        "levelds",
				"prefix":      "leveldb.datastore",
				"path":        "datastore",
				"compression": "none",
			},
		},
	}
}

func flatfsSpecMeasure() map[string]interface{} {
	return map[string]interface{}{
		"type": "mount",
		"mounts": []interface{}{
			map[string]interface{}{
				"mountpoint": "/blocks",
				"type":       "measure",
				"prefix":     "flatfs.datastore",
				"child": map[string]interface{}{
					"type":      "flatfs",
					"path":      "blocks",
					"sync":      false,
					"shardFunc": "/repo/flatfs/shard/v1/next-to-last/2",
				},
			},
			map[string]interface{}{
				"mountpoint": "/",
				"type":       "measure",
				"prefix":     "leveldb.datastore",
				"child": map[string]interface{}{
					"type":        "levelds",
					"path":        "datastore",
					"compression": "none",
				},
			},
		},
	}
}

// CreateIdentity initializes a new identity.
func CreateIdentity(out io.Writer, opts []options.KeyGenerateOption) (Identity, error) {
	// TODO guard higher up
	ident := Identity{}

	settings, err := options.KeyGenerateOptions(opts...)
	if err != nil {
		return ident, err
	}

	var sk crypto.PrivKey
	var pk crypto.PubKey

	switch settings.Algorithm {
	case "rsa":
		if settings.Size == -1 {
			settings.Size = options.DefaultRSALen
		}

		fmt.Fprintf(out, "generating %d-bit RSA keypair...", settings.Size)

		priv, pub, err := crypto.GenerateKeyPair(crypto.RSA, settings.Size)
		if err != nil {
			return ident, err
		}

		sk = priv
		pk = pub
	case "ed25519":
		if settings.Size != -1 {
			return ident, fmt.Errorf("number of key bits does not apply when using ed25519 keys")
		}
		fmt.Fprintf(out, "generating ED25519 keypair...")
		priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
		if err != nil {
			return ident, err
		}

		sk = priv
		pk = pub
	default:
		return ident, fmt.Errorf("unrecognized key type: %s", settings.Algorithm)
	}
	fmt.Fprintf(out, "done\n")

	// currently storing key unencrypted. in the future we need to encrypt it.
	// TODO(security)
	skbytes, err := crypto.MarshalPrivateKey(sk)
	if err != nil {
		return ident, err
	}
	ident.PrivKey = base64.StdEncoding.EncodeToString(skbytes)

	id, err := peer.IDFromPublicKey(pk)
	if err != nil {
		return ident, err
	}
	ident.PeerID = id.String()
	fmt.Fprintf(out, "peer identity: %s\n", ident.PeerID)
	return ident, nil
}
