package main

import (
	"cmp"
	"encoding/json"
	"errors"
	"log"
	"math/rand"
	"os"
	"slices"
	"strings"
	"sync"
	"time"
)

// Contact model

const (
	PageSize = 100
)

type anyMap map[string]any

/*
# mock contacts database
*/
var db = make(map[uint64]Contact, 0)

/*
class Contact:
*/
type Contact struct {
	ID     uint64  `json:"id"`
	First  string  `json:"first"`
	Last   string  `json:"last"`
	Phone  string  `json:"phone"`
	Email  string  `json:"email"`
	Errors []error `json:"errors"`
}

/*
def __init__(self, id_=None, first=None, last=None, phone=None, email=None):
*/
func NewContact(args anyMap) *Contact {
	c := Contact{
		Errors: make([]error, 0),
	}
	// Note: the "ok" are for the type assertions. The values are already set
	// to the 0V (or a slice for errors) at this point anyway.
	if id, ok := args["id"].(uint64); ok {
		c.ID = id
	}
	if first, ok := args["first"].(string); ok {
		c.First = first
	}
	if last, ok := args["last"].(string); ok {
		c.Last = last
	}
	if phone, ok := args["phone"].(string); ok {
		c.Phone = phone
	}
	if email, ok := args["email"].(string); ok {
		c.Email = email
	}
	return &c
}

/*
def __str__(self):
*/
func (self *Contact) String() string {
	bs, err := json.Marshal(*self)
	if err != nil {
		log.Printf("Error marshaling contact: %v", err)
	}
	return string(bs)
}

/*
def update(self, first, last, phone, email):
*/
func (self *Contact) Update(first, last, phone, email string) {
	self.First = first
	self.Last = last
	self.Phone = phone
	self.Email = email
}

/*
def validate(self):
*/
func (self *Contact) validate() bool {
	if self.Email == "" {
		self.Errors = append(self.Errors, errors.New("Email required"))
	}
	for id, contact := range db {
		if id != self.ID && contact.Email == self.Email {
			self.Errors = append(self.Errors, errors.New("Email Must Be Unique"))
			break
		}
	}
	return len(self.Errors) == 0
}

/*
def save(self):
*/
func (self *Contact) Save() bool {
	if !self.validate() {
		return false

	}
	var maxID uint64
	if self.ID == 0 {
		if len(db) == 0 {
			maxID = 1
		} else {
			for id := range db {
				if id > maxID {
					maxID = id
				}
			}
		}
		self.ID = maxID + 1
	}
	db[self.ID] = *self
	SaveDB()
	return true
}

/*
def delete(self):
*/
func (self *Contact) Delete() {
	delete(db, self.ID)
}

/*
@classmethod
def count(cls):
*/
func (*Contact) Count() int {
	time.Sleep(2 * time.Second)
	return len(db)
}

/*
@classmethod
def all(cls, page=1):
*/
func (*Contact) All(page int) []Contact {
	if page == 0 {
		page = 1
	}
	start := (page - 1) * PageSize
	end := min(start+PageSize, start+len(db))
	ids := make([]uint64, 0, len(db))
	for id := range db {
		ids = append(ids, id)
	}
	slices.SortStableFunc(ids, cmp.Compare[uint64])
	pageIDs := ids[start:end]
	contacts := make([]Contact, 0, len(pageIDs))
	for _, id := range pageIDs {
		contacts = append(contacts, db[id])
	}
	return contacts
}

/*
@classmethod
def search(cls, text):
*/
func (*Contact) Search(text string) []Contact {
	var result []Contact
	for _, c := range db {
		matchFirst := c.First != "" && strings.Contains(c.First, text)
		matchLast := c.Last != "" && strings.Contains(c.Last, text)
		matchEmail := c.Email != "" && strings.Contains(c.Email, text)
		matchPhone := c.Phone != "" && strings.Contains(c.Phone, text)
		if matchFirst || matchLast || matchEmail || matchPhone {
			result = append(result, c)
		}
	}
	return result
}

/*
@classmethod
def load_db(cls):
*/
func (*Contact) LoadDB() {
	db = make(map[uint64]Contact, 0)
	contacts := make([]Contact, 0)
	bs, err := os.ReadFile((&Archiver{}).ArchiveFile())
	if err != nil {
		panic(err)
	}
	if err := json.Unmarshal(bs, &contacts); err != nil {
		panic(err)
	}
	for _, c := range contacts {
		db[c.ID] = c
	}
}

/*
@staticmethod
def save_db():
*/
func SaveDB() {
	contacts := make([]Contact, 0, len(db))
	for _, contact := range db {
		contacts = append(contacts, contact)
	}
	slices.SortStableFunc(contacts, func(a, b Contact) int {
		return cmp.Compare(a.ID, b.ID)
	})
	f, err := os.Create((&Archiver{}).ArchiveFile())
	if err != nil {
		panic(err)
	}
	defer f.Close()
	enc := json.NewEncoder(f)
	if err := enc.Encode(contacts); err != nil {
		panic(err)
	}
}

/*
@classmethod
def find(cls, id_):
*/
func (*Contact) Find(id uint64) Contact {
	c, ok := db[id]
	// TODO shouldn't it be if !ok ? Original Python logic seems wrong.
	if ok {
		c.Errors = make([]error, 0)
	}
	return c
}

type Archiver struct {
	archiveStatus   string
	archiveProgress float64
	sync.RWMutex
}

const (
	statusWaiting  = "Waiting"
	statusRunning  = "Running"
	statusComplete = "Complete"
)

var (
	archiver *Archiver
)

/*
def status(self):
*/
func (self *Archiver) Status() string {
	self.RLock()
	defer self.RUnlock()
	return self.archiveStatus
}

/*
def progress(self):
*/
func (self *Archiver) Progress() float64 {
	self.RLock()
	defer self.RUnlock()
	return self.archiveProgress
}

/*
def run(self):
*/
func (self *Archiver) Run() {
	self.Lock()
	defer self.Unlock()
	if self.archiveStatus == statusWaiting {
		self.archiveStatus = statusRunning
		self.archiveProgress = 0.0
		go self.RunImpl()
	}
}

/*
def run_impl(self):
*/
func (self *Archiver) RunImpl() {
	t0 := time.Now()
	for i := range 10 {
		time.Sleep(time.Duration(float64(time.Second) * rand.Float64()))
		self.Lock()
		if self.archiveStatus != statusRunning {
			self.Unlock()
			return
		}
		self.archiveProgress = float64(i+1) / 10
		log.Printf("Here... after %v: %f", time.Since(t0), self.archiveProgress)
		self.Unlock()
	}
	time.Sleep(time.Second)
	self.Lock()
	if self.archiveStatus != statusRunning {
		self.Unlock()
		return
	}
	self.archiveStatus = statusComplete
	self.Unlock()
	log.Printf("RunImpl took %v", time.Since(t0))
}

/*
def archive_file(self):
*/
func (self *Archiver) ArchiveFile() string {
	return "contacts.json"
}

/*
def reset(self):
*/
func (self *Archiver) Reset() {
	self.Lock()
	defer self.Unlock()
	self.archiveStatus = statusWaiting
	self.archiveProgress = 0.0
}

/*
@classmethod
def get(cls):
*/
func GetArchiver() *Archiver {
	if archiver == nil {
		archiver = &Archiver{
			archiveStatus: statusWaiting,
		}
	}
	return archiver
}