package models

import (
	"crypto/rand"
	"fmt"
	"math/big"
	"net"
	"net/mail"
	"time"

	log "github.com/gophish/gophish/logger"
	"github.com/jinzhu/gorm"
	"github.com/oschwald/maxminddb-golang"
)

type mmCity struct {
	GeoPoint mmGeoPoint `maxminddb:"location"`
}

type mmGeoPoint struct {
	Latitude  float64 `maxminddb:"latitude"`
	Longitude float64 `maxminddb:"longitude"`
}

// Result contains the fields for a result object,
// which is a representation of a target in a campaign.
type Result struct {
	Id         int64     `json:"-"`
	CampaignId int64     `json:"-"`
	UserId     int64     `json:"-"`
	RId        string    `json:"id"`
	Email      string    `json:"email"`
	FirstName  string    `json:"first_name"`
	LastName   string    `json:"last_name"`
	Position   string    `json:"position"`
	Status     string    `json:"status" sql:"not null"`
	IP         string    `json:"ip"`
	Latitude   float64   `json:"latitude"`
	Longitude  float64   `json:"longitude"`
	SendDate   time.Time `json:"send_date"`
	Reported   bool      `json:"reported" sql:"not null"`
}

// UpdateStatus updates the status of the result in the database
func (r *Result) UpdateStatus(s string) error {
	return db.Table("results").Where("id=?", r.Id).Update("status", s).Error
}

// UpdateReported updates when a user reports a campaign
func (r *Result) UpdateReported(s bool) error {
	return db.Table("results").Where("id=?", r.Id).Update("reported", s).Error
}

// UpdateGeo updates the latitude and longitude of the result in
// the database given an IP address
func (r *Result) UpdateGeo(addr string) error {
	// Open a connection to the maxmind db
	mmdb, err := maxminddb.Open("static/db/geolite2-city.mmdb")
	if err != nil {
		log.Fatal(err)
	}
	defer mmdb.Close()
	ip := net.ParseIP(addr)
	var city mmCity
	// Get the record
	err = mmdb.Lookup(ip, &city)
	if err != nil {
		return err
	}
	// Update the database with the record information
	return db.Table("results").Where("id=?", r.Id).Updates(map[string]interface{}{
		"ip":        addr,
		"latitude":  city.GeoPoint.Latitude,
		"longitude": city.GeoPoint.Longitude,
	}).Error
}

// GenerateId generates a unique key to represent the result
// in the database
func (r *Result) GenerateId() error {
	// Keep trying until we generate a unique key (shouldn't take more than one or two iterations)
	const alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	k := make([]byte, 7)
	for {
		for i := range k {
			idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphaNum))))
			if err != nil {
				return err
			}
			k[i] = alphaNum[idx.Int64()]
		}
		r.RId = string(k)
		err := db.Table("results").Where("r_id=?", r.RId).First(&Result{}).Error
		if err == gorm.ErrRecordNotFound {
			break
		}
	}
	return nil
}

// FormatAddress returns the email address to use in the "To" header of the email
func (r *Result) FormatAddress() string {
	addr := r.Email
	if r.FirstName != "" && r.LastName != "" {
		a := &mail.Address{
			Name:    fmt.Sprintf("%s %s", r.FirstName, r.LastName),
			Address: r.Email,
		}
		addr = a.String()
	}
	return addr
}

// GetResult returns the Result object from the database
// given the ResultId
func GetResult(rid string) (Result, error) {
	r := Result{}
	err := db.Where("r_id=?", rid).First(&r).Error
	return r, err
}
