package spy

// |@@| C

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"gardening/src/lib"
	"gardening/src/lib/jsoner"
	"os"
	"runtime"
	"strings"
)

type Spy struct {
	Jsoner *jsoner.Jsoner
}

type SpyingMod struct {
}

type Report struct {
	Hash    string    `json:"hash"`
	Records []*Record `json:"records"`
	Systems []*System `json:"systems"`
}

type System struct {
	Key        string `json:"key"`
	Read       bool   `json:"read"`
	Write      bool   `json:"write"`
	ReadCount  int    `json:"read_count"`
	WriteCount int    `json:"write_count"`
}

type Record struct {
	Kind   string `json:"kind"`
	System string `json:"system"`
	Data   any    `json:"data"`
	Hash   string `json:"hash"`
}

const CtxKeyActive = "lib:spy:active"
const CtxKeyReport = "lib:spy:report"

const RecordKindHandlerCreate = "lib:handler:create"
const RecordKindHandlerUpdate = "lib:handler:update"
const RecordKindHandlerDelete = "lib:handler:delete"

const SystemDb = "db"
const SystemFilesystem = "filesystem"
const SystemHttpClient = "http_client"

func (this *Spy) SpyOn(ctx context.Context) context.Context {
	ctx = context.WithValue(ctx, CtxKeyActive, true)
	ctx = context.WithValue(ctx, CtxKeyReport, &Report{})
	return ctx
}

func (this *Spy) SpyOff(ctx context.Context) context.Context {
	return context.WithValue(ctx, CtxKeyActive, nil)
}

func (this *Spy) Spying(ctx context.Context, spyOnFunction func(context.Context), mod *SpyingMod) *Report {
	spyCtx := this.SpyOn(ctx)
	spyOnFunction(spyCtx)
	return this.GetReportFromCtx(spyCtx)
}

func (this *Spy) IsActive(ctx context.Context) bool {
	v := ctx.Value(CtxKeyActive)
	if v == nil {
		return false
	}
	value, ok := v.(bool)
	if !ok {
		return false
	}
	return value
}

func (this *Spy) LogRecord(ctx context.Context, kind string, data any) {
	if !this.IsActive(ctx) {
		return
	}
	report := this.GetReportFromCtx(ctx)
	record := &Record{Kind: kind, Data: data}
	record.Hash = this.generateRecordHash(ctx, record)
	report.Records = append(report.Records, record)
}

func (this *Spy) GetReportFromCtx(ctx context.Context) *Report {
	v := ctx.Value(CtxKeyReport)
	if v == nil {
		panic("No spy report instance in ctx")
	}
	report, ok := v.(*Report)
	if !ok {
		panic("No spy report instance in ctx")
	}
	report.Hash = this.generateReportHash(ctx, report)
	report.Systems = this.computeSystemStats(ctx, report)
	return report
}

func (this *Spy) GetReportHashFromCtx(ctx context.Context) string {
	report := this.GetReportFromCtx(ctx)
	return report.Hash
}

func (this *Spy) ConvertReportToString(ctx context.Context, report *Report) string {
	b, err := json.Marshal(report)
	lib.Poe(err)
	return string(b)
}

func (this *Spy) generateReportHash(ctx context.Context, report *Report) string {
	b := this.Jsoner.MustMarshal(report.Records)
	hash := sha256.Sum256(b)
	return fmt.Sprintf("%x", hash[0:12])
}

func (this *Spy) generateRecordHash(ctx context.Context, record *Record) string {
	b := this.Jsoner.MustMarshal(record)
	hash := sha256.Sum256(b)
	return fmt.Sprintf("%x", hash[0:12])
}

func (this *Spy) CheckAgainstReference(ctx context.Context, report *Report) bool {
	_, currentFilePath, _, _ := runtime.Caller(1)
	dataDir := strings.TrimSuffix(currentFilePath, ".go")
	referenceFile := dataDir + "/reference.report.json"
	if _, err := os.Stat(referenceFile); errors.Is(err, os.ErrNotExist) {
		println("Reference created " + referenceFile)
		this.createReference(dataDir, report, referenceFile)
	}
	b, err := os.ReadFile(referenceFile)
	lib.Poe(err)
	b2 := this.Jsoner.MustMarshal(report)
	if string(b2) == string(b) {
		return true
	}
	//os.WriteFile("/tmp/a.json", b2, 0644)
	//os.WriteFile("/tmp/b.json", b, 0644)
	return false
}

func (this *Spy) createReference(dataDir string, report *Report, referenceFile string) {
	err := os.MkdirAll(dataDir, 0744)
	lib.Poe(err)
	b := this.Jsoner.MustMarshal(report)
	err = os.WriteFile(referenceFile, b, 0644)
	lib.Poe(err)
}

func (this *Spy) computeSystemStats(ctx context.Context, report *Report) []*System {
	m := make(map[string]*System)
	m[SystemDb] = &System{Key: SystemDb}
	m[SystemFilesystem] = &System{Key: SystemFilesystem}
	m[SystemHttpClient] = &System{Key: SystemHttpClient}
	for _, record := range report.Records {
		if record.Kind == RecordKindHandlerCreate ||
			record.Kind == RecordKindHandlerUpdate ||
			record.Kind == RecordKindHandlerDelete {
			m[SystemDb].Write = true
			m[SystemDb].WriteCount++
		}
	}
	for _, record := range report.Records {
		if record.Kind == "write-file" {
			m[SystemFilesystem].Write = true
			m[SystemFilesystem].WriteCount++
		}
	}
	var set []*System
	for _, system := range m {
		set = append(set, system)
	}
	return set
}
