package job

import (
	"encoding/json"
	"fmt"
	"log"
	"math/rand"
	"os"
	"strings"
	"sync"
	"time"
)

// Job holds the job/process metadata
type Job struct {
	lock sync.Mutex // must hold WriteLock when changing status info

	ID          uint64  `json:"jobID"`       // unique ID of this job
	Name        string  `json:"name"`        // a name for this job
	Queue       string  `json:"queue"`       // which queue this job
	StartTime   int64   `json:"-"`           // time.Now().UnixMilli() at job creation
	TokenName   string  `json:"tokenName"`   // name of the token to decrement for this job (if any)
	TokenCost   float64 `json:"tokenCost"`   // the token cost of this job
	App         string  `json:"app"`         // app that started this job
	UID         string  `json:"uid"`         // uid that started this job
	Email       string  `json:"email"`       // email that started this job
	Entitlement bool    `json:"entitlement"` // the entitlement of the uid that start this job
	Version     string  `json:"version"`     // version of the app that started this job
	Hint        string  `json:"hint"`        // the workflow hint for scheduling optimization
	HintMatched bool    `json:"hintMatched"` // true if the hint was used to assign this job

	// inputs and outputs are just map of name and value pairs
	// values can be anything, strings, ints, floats, lists, maps
	// values can also be files which are handled specially because they aren't JSON serializable
	Inputs  map[string]any `json:"inputs"`
	Outputs map[string]any `json:"outputs"`

	// status information (must hold lock while accessing)
	status         string   `json:"-"`
	statusTime     int64    `json:"-"` // time.Now().UnixMilli() when job status last changed
	lastCheckTime  int64    `json:"-"` // time.Now().UnixMilli() at the last time the status was checked
	assignedTime   int64    `json:"-"` // time.Now().UnixMilli() when job assigned to worker
	assignedWorker string   `json:"-"` // the worker assigned to complete the job
	queueTime      int64    `json:"-"` // time.Now().UnixMilli() when job assigned to a queue
	assignedQueue  string   `json:"-"` // the queue this job was put in
	completed      chan int `json:"-"` // closed on job completion (to error or complete)
	completionTime int64    `json:"-"` // time.Now().UnixMilli() when job was completed (or errored out)
}

// OutputFile stores the metadata for an file "output" of a model
type OutputFile struct {
	Filename    string `json:"filename"`    // the localfilename
	Savename    string `json:"savename"`    // the name to save on the callers system
	ContentType string `json:"contentType"` // the http content type
}

var jobLogFile *os.File = nil
var jobLogFileMutex sync.Mutex

var jobTable = make(map[uint64]*Job)
var jobTableLock sync.Mutex
var nextJobID uint64 = 0

func Init(jobFilePtr *string) {
	if jobFilePtr != nil && *jobFilePtr != "" {
		var err error
		jobLogFile, err = os.OpenFile(*jobFilePtr, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			log.Printf("Error creating job log file: %s\n", err.Error())
		}
	}

}

// GetJob returns a job with the specified ID
func GetJob(id uint64) *Job {
	jobTableLock.Lock()
	defer jobTableLock.Unlock()
	return jobTable[id]
}

// CreateJob creates a new job and adds it to the job table and returns a ref
func CreateJob(name string, app string, uid string, email string, entitlement bool, version string) *Job {
	jobID := nextID()
	job := Job{
		ID:            jobID,
		Name:          name,
		App:           app,
		UID:           uid,
		Email:         email,
		Entitlement:   entitlement,
		Version:       version,
		Hint:          "",
		HintMatched:   false,
		StartTime:     time.Now().UnixMilli(),
		Inputs:        make(map[string]any),
		Outputs:       make(map[string]any),
		status:        "queued",
		statusTime:    time.Now().UnixMilli(),
		lastCheckTime: time.Now().UnixMilli(),
		completed:     make(chan int),
	}
	jobTableLock.Lock()
	jobTable[jobID] = &job
	jobTableLock.Unlock()

	return &job
}

// FullName returns an official name for the job for better printing and debugging
func (j *Job) FullName() string {
	return fmt.Sprintf("[%d,%-9.9s]", j.ID, j.Name)
}

// UpdateStatus changes the status information
// returns the time since the job was assigned
func (j *Job) UpdateStatus(status string, outputs map[string]any) int64 {
	j.lock.Lock()
	defer j.lock.Unlock()
	now := time.Now().UnixMilli()
	if j.completed == nil {
		log.Printf("Job %s: Error: cannot update status of completed job (%v %v)\n", j.FullName(), j.status, status)
		return 0
	}
	if j.status != status {
		log.Printf("Job %s: %15s -> %.15s\n", j.FullName(), j.status, status)
		if status == "error" {
			errstr := ""
			if outputs != nil {
				errstr = strings.ReplaceAll(fmt.Sprintf("%v", outputs["error"]), "\n", "")
			}
			log.Printf("Job %s: Error: \"%.70s\" [%s] [%s,%s]\n", j.FullName(), errstr, j.assignedWorker, j.App, j.Version)
		}

		j.status = status
		j.statusTime = now
		elapsedTime := now - j.assignedTime
		if status == "complete" || status == "error" {
			if j.completed != nil {
				close(j.completed)
				j.completed = nil
				j.completionTime = time.Now().UnixMilli()
				j.LogJobComplete()
			}
		}
		return elapsedTime
	}
	return 0
}

// AssignWorker assign a job to a worker
func (j *Job) AssignWorker(workerName string) {
	j.lock.Lock()
	defer j.lock.Unlock()
	j.assignedTime = time.Now().UnixMilli()
	j.assignedWorker = workerName
}

// AssignQueue assign a job to a wait queue
func (j *Job) AssignQueue(queueName string) {
	j.lock.Lock()
	defer j.lock.Unlock()
	j.queueTime = time.Now().UnixMilli()
	j.assignedQueue = queueName
}

// Completed returns true if the job is completed (either "complete" or "error")
func (j *Job) Completed() bool {
	j.lock.Lock()
	complete := (j.completed == nil)
	status := j.status
	j.lock.Unlock()

	if complete && (status != "complete" && status != "error") {
		log.Printf("Constraint failed: Job %d complete but status is %s\n", j.ID, j.status)
	}
	return complete
}

// WaitForCompletion waits until the timeout
func (j *Job) WaitForCompletion(timeoutSec float64) {
	j.lock.Lock()
	c := j.completed
	j.lock.Unlock()
	if c == nil {
		return
	}
	select {
	case <-c:
		return
	case <-time.After(time.Duration(timeoutSec) * time.Second):
		return
	}
}

// GetStatusJson returns a json obj representing the status of the job
func (j *Job) GetStatusJson() map[string]any {
	var jmap = make(map[string]any)
	j.lock.Lock()
	j.lastCheckTime = time.Now().UnixMilli()
	jmap["jobID"] = j.ID
	jmap["status"] = j.status
	j.lock.Unlock()

	return jmap
}

// ToJson returns a json obj representing the job
func (j *Job) ToJson() (map[string]any, error) {
	jstr, err := json.MarshalIndent(j, "", "  ")
	if err != nil {
		log.Printf("Error serializing job: %s\n", err.Error())
		return nil, err
	}

	var mapData map[string]any
	err = json.Unmarshal(jstr, &mapData)
	if err != nil {
		log.Printf("Error unmarshalling JSON to map: %s\n", err.Error())
		return nil, err
	}

	j.lock.Lock()
	mapData["assigned"] = j.assignedWorker
	mapData["status"] = j.status
	j.lock.Unlock()

	return mapData, nil
}

// GetStatus returns just the status string
func (j *Job) GetStatus() string {
	j.lock.Lock()
	defer j.lock.Unlock()
	return j.status
}

// GetAssignedQueue returns just the assignedQueue string
func (j *Job) GetAssignedQueue() string {
	j.lock.Lock()
	defer j.lock.Unlock()
	return j.assignedQueue
}

// GetQueueTime returns the time this job was assigned to a queue
func (j *Job) GetQueueTime() int64 {
	j.lock.Lock()
	defer j.lock.Unlock()
	return j.queueTime
}

// GetLastCheckTime returns the last time this job was checked by the client
func (j *Job) GetLastCheckTime() int64 {
	j.lock.Lock()
	defer j.lock.Unlock()
	return j.lastCheckTime
}

// GetAssignedTime returns the time this job was assigned (or 0)
func (j *Job) GetAssignedTime() int64 {
	j.lock.Lock()
	defer j.lock.Unlock()
	return j.assignedTime
}

// GetAssignedWorker returns the assigned worker (or "" is none)
func (j *Job) GetAssignedWorker() string {
	j.lock.Lock()
	defer j.lock.Unlock()
	return j.assignedWorker
}

// Log job queued to logfile
func (j *Job) LogJobQueued() {
	if jobLogFile == nil {
		return
	}
	timeFormatted := time.Now().Format("2006-01-02T15:04:05")
	str := fmt.Sprintf("%s: %v QUEUE %s %s %s | queued\n", timeFormatted, j.ID, j.Name, j.Hint, j.Email)
	jobLogFileMutex.Lock()
	_, err := jobLogFile.WriteString(str)
	jobLogFileMutex.Unlock()
	if err != nil {
		log.Printf("Log Error: %s\n", err.Error())
	}
}

// Log job start to logfile
func (j *Job) LogJobStart() {
	if jobLogFile == nil {
		return
	}
	timeFormatted := time.Now().Format("2006-01-02T15:04:05")
	elapsed := float64(j.assignedTime-j.queueTime) / 1000.0
	str := fmt.Sprintf("%s: %v START %s %s %s | worker: %s hint_match: %t queue_time: %.1f\n", timeFormatted, j.ID, j.Name, j.Hint, j.Email, j.assignedWorker, j.HintMatched, elapsed)
	jobLogFileMutex.Lock()
	_, err := jobLogFile.WriteString(str)
	jobLogFileMutex.Unlock()
	if err != nil {
		log.Printf("Log Error: %s\n", err.Error())
	}
}

// Log job complete to logfile
func (j *Job) LogJobComplete() {
	if jobLogFile == nil {
		return
	}
	timeFormatted := time.Now().Format("2006-01-02T15:04:05")
	elapsed := float64(j.completionTime-j.assignedTime) / 1000.0
	str := fmt.Sprintf("%s: %v DONE  %s %s %s | worker: %s process_time: %.1f\n", timeFormatted, j.ID, j.Name, j.Hint, j.Email, j.assignedWorker, elapsed)
	jobLogFileMutex.Lock()
	_, err := jobLogFile.WriteString(str)
	jobLogFileMutex.Unlock()
	if err != nil {
		log.Printf("Log Error: %s\n", err.Error())
	}
}

// Save a job to the disk
func (j *Job) SaveJob(filesDir string) {
	jobj, err := j.ToJson()
	if err != nil {
		log.Printf("Error serializing job: %s\n", err.Error())
		return
	}
	js, err := json.MarshalIndent(jobj, "", "  ")
	filename := fmt.Sprintf("%s/%s/%v.json", filesDir, j.Name, j.ID)
	if err != nil {
		log.Printf("Error serializing job: %s\n", err.Error())
		return
	} else {
		err = os.WriteFile(filename, js, 0644)
		if err != nil {
			log.Printf("Error saving job: %s\n", err.Error())
			return
		}
	}
}

// JobCleaner goes through jobs and just removes old completed jobs from jobTable
func JobCleaner() {
	ticker := time.NewTicker(60 * time.Second)
	for range ticker.C {
		var cutoffMin int64 = 10
		now := time.Now().UnixMilli()

		jobTableLock.Lock()
		for id, job := range jobTable {
			job.lock.Lock()
			if job.completionTime != 0 && now-job.completionTime > 1000*60*cutoffMin {
				//log.Printf("Deleting old completed job: %d %s (%d seconds old)\n", id, job.status, (now-job.completionTime)/1000)
				delete(jobTable, id)
			}
			job.lock.Unlock()
		}
		jobTableLock.Unlock()
	}
}

// nextID returns the next unique available job ID
func nextID() uint64 {
	jobTableLock.Lock()
	defer jobTableLock.Unlock()
	if nextJobID == 0 {
		nextJobID = uint64(time.Now().UnixMilli() * 10)
	}
	jobID := nextJobID
	nextJobID = nextJobID + (rand.Uint64() % 1000000000)
	//nextJobID = nextJobID + 1
	return jobID
}
