package main

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

type ContainerInfo struct {
	ID       string
	Name     string
	CName    string
	Interval string
	Ignores  []string
}

type MonitoredContainer struct {
	Info        ContainerInfo
	LastChecked time.Time
}

var (
	apiAddress          string
	glitchtipDSN        string
	notifiedChanges     = make(map[string]string)
	monitoredContainers = make(map[string]*MonitoredContainer)
	mu                  sync.Mutex
)

func init() {
	apiAddress = os.Getenv("API_ADDRESS")
	if apiAddress == "" {
		apiAddress = "http://localhost:8080"
	}

	glitchtipDSN = os.Getenv("GLITCHTIP_DSN")
	if glitchtipDSN == "" {
		log.Fatal("GLITCHTIP_DSN environment variable is not set")
	}
}

func main() {
	log.Println("Starting Oculus...")

	// Ensure the diffs directory exists
	err := os.MkdirAll("./diffs", os.ModePerm)
	if err != nil {
		log.Fatalf("Error creating diffs directory: %v", err)
	}

	for {
		err := fetchAndMonitorContainers()
		if err != nil {
			log.Printf("Error in fetching and monitoring containers: %v", err)
		}

		time.Sleep(1 * time.Second)
	}
}

func fetchAndMonitorContainers() error {
	containers, err := fetchContainers()
	if err != nil {
		return err
	}

	for _, container := range containers {
		mu.Lock()
		if _, exists := monitoredContainers[container.ID]; !exists {
			interval, err := time.ParseDuration(container.Interval)
			if err != nil {
				log.Printf("Invalid interval for container %s (%s): %v", container.Name, container.ID, err)
				mu.Unlock()
				continue
			}

			monitoredContainers[container.ID] = &MonitoredContainer{
				Info:        container,
				LastChecked: time.Now().Add(-interval),
			}
		}
		mu.Unlock()
	}

	var wg sync.WaitGroup
	for _, monitoredContainer := range monitoredContainers {
		wg.Add(1)
		go func(mc *MonitoredContainer) {
			defer wg.Done()
			interval, err := time.ParseDuration(mc.Info.Interval)
			if err != nil {
				log.Printf("Invalid interval for container %s (%s): %v", mc.Info.Name, mc.Info.ID, err)
				return
			}

			mu.Lock()
			if time.Since(mc.LastChecked) >= interval {
				mc.LastChecked = time.Now()
				mu.Unlock()
				checkAndNotify(mc.Info)
			} else {
				mu.Unlock()
			}
		}(monitoredContainer)
	}
	wg.Wait()

	return nil
}

func fetchContainers() ([]ContainerInfo, error) {
	resp, err := http.Get(fmt.Sprintf("%s/containers", apiAddress))
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("error fetching containers: %s", resp.Status)
	}

	var containers []ContainerInfo
	if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
		return nil, err
	}

	return containers, nil
}

func fetchDiffOutput(containerID string) (string, error) {
	resp, err := http.Get(fmt.Sprintf("%s/diff?id=%s", apiAddress, containerID))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("error fetching diff: %s", resp.Status)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(body), nil
}

func checkAndNotify(container ContainerInfo) {
	diffOutput, err := fetchDiffOutput(container.ID)
	if err != nil {
		log.Printf("Error getting diffs for container %s (%s): %v", container.Name, container.ID, err)
		return
	}

	filteredOutput, err := filterDiffOutput(diffOutput, container.CName, container.Ignores)
	if err != nil {
		log.Printf("Error filtering diffs for container %s (%s): %v", container.Name, container.ID, err)
		return
	}
	log.Printf("Filtered diff output for container %s: %s", container.Name, filteredOutput)

	if filteredOutput != "" {
		diffHash := fmt.Sprintf("%x", md5.Sum([]byte(filteredOutput)))
		log.Printf("Diff hash for container %s: %s", container.Name, diffHash)

		mu.Lock()
		lastNotifiedHash, notified := notifiedChanges[container.ID]
		mu.Unlock()

		if !notified || lastNotifiedHash != diffHash {
			filename := filepath.Join("diffs", fmt.Sprintf("%s.diff", container.CName))
			err := writeToFile(filename, filteredOutput)
			if err != nil {
				log.Printf("Error writing diff to file: %v", err)
				return
			}

			err = sendNotification(filteredOutput)
			if err != nil {
				log.Printf("Error sending notification for container %s: %v", container.ID, err)
			} else {
				mu.Lock()
				notifiedChanges[container.ID] = diffHash
				mu.Unlock()
				log.Printf("Notification sent and hash updated for container: %s (%s)", container.Name, container.ID)
			}
		} else {
			log.Printf("No new changes detected for container: %s (%s)", container.Name, container.ID)
		}
	} else {
		log.Printf("No significant changes detected for container: %s (%s)", container.Name, container.ID)
	}
}

func filterDiffOutput(diffOutput, cname string, ignores []string) (string, error) {
	filename := filepath.Join("diffs", fmt.Sprintf("%s.diff", cname))

	// Write the diff output to the file
	err := ioutil.WriteFile(filename, []byte(diffOutput), 0644)
	if err != nil {
		return "", fmt.Errorf("error writing diff to file: %s", err)
	}

	// Construct the filter command
	args := append([]string{filename}, ignores...)
	cmd := exec.Command("filter", args...)
	fullCommand := fmt.Sprintf("filter %s", strings.Join(cmd.Args, " "))
	log.Printf("Running command: %s", fullCommand)
	fmt.Printf("Running command: %s\n", fullCommand) // Print the command to stdout for debugging

	output, err := cmd.CombinedOutput()
	if err != nil {
		return "", fmt.Errorf("error running filter command: %s, output: %s", err, output)
	}

	// Read the filtered output
	filteredOutput, err := ioutil.ReadFile(filename)
	if err != nil {
		return "", fmt.Errorf("error reading filtered diff file: %s", err)
	}

	return string(filteredOutput), nil
}

func writeToFile(filename, content string) error {
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = file.WriteString(content)
	return err
}

func sendNotification(content string) error {
	cmd := exec.Command("go-glitch")
	cmd.Stdin = strings.NewReader(content)
	output, err := cmd.CombinedOutput()
	if err != nil {
		log.Printf("go-glitch output: %s", output)
		return err
	}
	return nil
}
