package main

import (
	"archive/zip"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"syscall"
	"time"
)

func init() {
	logFile, err := os.OpenFile(".bckr.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	if err != nil {
		log.Fatal(err)
	}
	log.SetPrefix("bckr | ")
	log.SetOutput(logFile)
}

func main() {

	// Если процесс процесс с именем этого исполняемого файла уже запущен
	// то закрываем и его и текущий процесс
	exeName := filepath.Base(os.Args[0])
	if isAlreadyRunning(exeName) {
		log.Println("Already running. Shutdown...")
		if err := killProcByExeName(exeName); err != nil {
			log.Println("Cant kill proc")
		}
		os.Exit(0)
	}

	hideConsoleWindow()

	for {

		// Загрузка конфига
		config, err := loadConfig(".bckr.json")
		if err != nil {
			log.Fatal(err)
		}
		//

		// Создание архива
		archiveName := "backup_" + time.Now().Format("2006-01-02_15-04-05") + ".zip"
		if err := createArchive(archiveName, config.TargetsFilesOrDirs, config.IgnoreFilesOrDirs); err != nil {
			log.Println(err)
			goto sleep
		}
		log.Println("Archive created successfully:", archiveName)
		//

		// Отправка архива
		if err := TGBotSendFile(config.TGYourBotToken, config.TGYourUserId, archiveName, ""); err != nil {
			log.Println(err)
			goto sleep
		}
		log.Println("Archive sent successfully:", archiveName)
		//

		// Сон
	sleep:
		os.Remove(archiveName)
		time.Sleep(time.Duration(config.IntervalInSeconds) * time.Second)
		//
	}

}

type config struct {
	TGYourBotToken     string
	TGYourUserId       string
	IntervalInSeconds  uint64
	TargetsFilesOrDirs []string
	IgnoreFilesOrDirs  []string
}

func loadConfig(configPath string) (*config, error) {
	cfg := &config{}
	configFile, err := os.ReadFile(configPath)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(configFile, cfg)
	if err != nil {
		return nil, err
	}
	return cfg, nil
}

func createArchive(archiveName string, targets, ignore []string) error {
	// Создание файла архива
	archiveFile, err := os.Create(archiveName)
	if err != nil {
		return err
	}
	defer archiveFile.Close()
	//

	// Полный путь до архива
	archiveFileAbsPath, err := filepath.Abs(archiveName)
	if err != nil {
		return err
	}
	//

	// writer для ZIP-архива
	zipWriter := zip.NewWriter(archiveFile)
	defer zipWriter.Close()
	//

	// Преобразуем ignore-пути в абсолютные для сравнения
	var ignorePaths []string
	for _, path := range ignore {
		absPath, err := filepath.Abs(path)
		if err != nil {
			return err
		}
		ignorePaths = append(ignorePaths, absPath)
	}
	// И не архивируем сами себя
	ignorePaths = append(ignorePaths, archiveFileAbsPath)
	//

	// Проверяет, нужно ли игнорировать путь
	shouldIgnore := func(path string) bool {
		absPath, err := filepath.Abs(path)
		if err != nil {
			return false
		}

		if slices.Contains(ignorePaths, absPath) {
			return true
		}

		return false
	}
	//

	// Обрабатываем каждый целевой путь
	for _, target := range targets {
		// Проверяем существование пути
		if _, err := os.Stat(target); os.IsNotExist(err) {
			log.Printf("Warning: target path does not exist: %s", target)
			continue
		}

		// Рекурсивно обходим начиная с target
		err := filepath.Walk(target, func(filePath string, fileInfo os.FileInfo, err error) error {
			if err != nil {
				// ошибка доступа
				return err
			}

			// Пропускаем игнорируемые пути
			if shouldIgnore(filePath) {
				if fileInfo.IsDir() {
					return filepath.SkipDir // Пропускаем всю директорию
				}
				return nil // Пропускаем файл
			}

			// Создаем заголовок файла в архиве
			header, err := zip.FileInfoHeader(fileInfo)
			if err != nil {
				return err
			}

			// Устанавливаем правильное имя файла в архиве
			header.Name, err = filepath.Rel(filepath.Dir(target), filePath)
			if err != nil {
				return err
			}

			// Для директорий добавляем слеш в конец
			if fileInfo.IsDir() {
				header.Name += "/"
			} else {
				// Устанавливаем метод сжатия для файликов
				header.Method = zip.Deflate
			}

			// Создаем запись в архиве
			writer, err := zipWriter.CreateHeader(header)
			if err != nil {
				return err
			}

			// Если это файл - копируем его содержимое
			if !fileInfo.IsDir() {
				file, err := os.Open(filePath)
				if err != nil {
					return err
				}
				defer file.Close()

				_, err = io.Copy(writer, file)
				if err != nil {
					return err
				}
			}

			return nil
		})

		if err != nil {
			return err
		}
	}

	return nil
}

func TGBotSendFile(botToken, userId, filePath, caption string) error {
	apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendDocument", botToken)

	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	body := &bytes.Buffer{}

	writer := multipart.NewWriter(body)

	part, err := writer.CreateFormFile("document", filepath.Base(file.Name()))
	if err != nil {
		return err
	}

	_, err = io.Copy(part, file)
	if err != nil {
		return err
	}

	writer.WriteField("chat_id", userId)
	writer.WriteField("caption", caption)

	err = writer.Close()
	if err != nil {
		return err
	}

	req, err := http.NewRequest("POST", apiURL, body)
	if err != nil {
		return err
	}

	req.Header.Set("Content-Type", writer.FormDataContentType())

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	return nil
}

func killProcByExeName(exeName string) error {
	if runtime.GOOS == "windows" {
		cmd := exec.Command("taskkill", "/IM", exeName, "/F", "/T")
		_, err := cmd.Output()
		return err
	} else {
		cmd := exec.Command("pkill", "-f", exeName)
		err := cmd.Run()
		return err
	}
}

func isAlreadyRunning(exeName string) bool {
	if runtime.GOOS == "windows" {
		cmd := exec.Command("tasklist", "/FI", "IMAGENAME eq "+exeName, "/FO", "CSV", "/NH")
		output, err := cmd.Output()
		return err == nil && strings.Count(string(output), "\n") > 1
	} else {
		return false
	}
}

func hideConsoleWindow() {
	if runtime.GOOS == "windows" {
		hideConsoleWindowWindows()
	} else if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
		hideConsoleWindowUnix()
	}
}

func hideConsoleWindowWindows() {
	kernel32 := syscall.NewLazyDLL("kernel32.dll")
	proc := kernel32.NewProc("FreeConsole")
	proc.Call()
}

func hideConsoleWindowUnix() {
	cmd := exec.Command("nohup", os.Args[0])
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Start()
	os.Exit(0)
}
