package ipmeta

import (
	"context"
	"fmt"
	"io"
	"net"
	"os"
	"sync"
	"time"

	"github.com/ip2location/ip2location-go"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
	"github.com/scraperwall/asndb/v2"

	log "github.com/sirupsen/logrus"
)

type IPs []string
type Meta struct {
	IP          string
	IP2Loc      ip2location.IP2Locationrecord
	ASN         *asndb.ASN
	ASNError    error
	IP2LocError error
}

type IPMeta struct {
	asndb    *asndb.DB
	ip2locdb *ip2location.DB

	mutex   sync.RWMutex
	options IPMetaOpts
}

type IPMetaOpts struct {
	S3Endpoint     string
	S3Bucket       string
	S3Key          string
	S33Secret      string
	S3Object       string
	IP2LocFile     string
	AsnDBURL       string
	UpdateInterval time.Duration
}

func (i *IPMeta) load(ctx context.Context, force bool) error {

	var i2ldb *ip2location.DB
	var asnDB *asndb.DB
	var err error

	// Load IP2Loc database
	//
	if i.options.S33Secret != "" &&
		i.options.S3Bucket != "" &&
		i.options.S3Endpoint != "" &&
		i.options.S3Key != "" &&
		i.options.S3Object != "" &&
		i.options.IP2LocFile != "" {

		stat, err := os.Stat(i.options.IP2LocFile)

		if force || err != nil || stat.ModTime().Before(time.Now().Add(-1*i.options.UpdateInterval)) {
			mc, err := minio.New(i.options.S3Endpoint, &minio.Options{
				Creds:  credentials.NewStaticV4(i.options.S3Key, i.options.S33Secret, ""),
				Secure: true,
			})
			if err != nil {
				return err
			}

			obj, err := mc.GetObject(ctx, i.options.S3Bucket, i.options.S3Object, minio.GetObjectOptions{})
			if err != nil {
				return err
			}

			fh, err := os.OpenFile(i.options.IP2LocFile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666)
			if err != nil {
				return fmt.Errorf("%s: %s", i.options.IP2LocFile, err)
			}

			_, err = io.Copy(fh, obj)
			if err != nil {
				return fmt.Errorf("writing %s failed: %s", i.options.IP2LocFile, err)
			}

			err = obj.Close()
			if err != nil {
				return err
			}

			err = fh.Close()
			if err != nil {
				return err
			}
		}

		i2ldb, err = ip2location.OpenDB(i.options.IP2LocFile)
		if err != nil {
			return fmt.Errorf("failed to open IP2Location DB: %s", err)
		}
	}

	if i.options.AsnDBURL != "" {
		// Load the ASN database
		//
		asnDB, err = asndb.New(i.options.AsnDBURL)
		if err != nil {
			return fmt.Errorf("failed to load ASNDB from %s: %s", i.options.AsnDBURL, err)
		}

	}

	i.mutex.Lock()
	i.ip2locdb = i2ldb
	i.asndb = asnDB
	i.mutex.Unlock()

	return nil
}

func (i *IPMeta) reload(ctx context.Context) {
	ticker := time.NewTicker(i.options.UpdateInterval)

	for {
		select {
		case <-ctx.Done():
			ticker.Stop()
			break
		case <-ticker.C:
			err := i.load(ctx, true)
			if err != nil {
				log.Println(err)
			}
		}
	}
}

func (i *IPMeta) Lookup(ip net.IP) *Meta {
	meta := Meta{IP: ip.String()}

	i.mutex.RLock()
	defer i.mutex.RUnlock()

	if i.asndb != nil {
		meta.ASN = i.asndb.Lookup(ip)
		if meta.ASN == nil {
			meta.ASNError = fmt.Errorf("no ASN record found for %s", ip)
		}
	}

	if i.ip2locdb != nil {
		rec, err := i.ip2locdb.Get_all(ip.String())
		meta.IP2LocError = err
		meta.IP2Loc = rec
	}

	return &meta
}

func New(ctx context.Context, opts IPMetaOpts) (*IPMeta, error) {
	i2l := IPMeta{
		mutex:   sync.RWMutex{},
		options: opts,
	}

	err := i2l.load(ctx, false)
	if err != nil {
		return nil, err
	}

	if i2l.options.UpdateInterval > time.Hour {
		go i2l.reload(ctx)
	}

	return &i2l, nil
}
