package main

import (
	"archive/zip"
	"bufio"
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
)

func main() {
	syncUpCmd := flag.NewFlagSet("sync-up", flag.ExitOnError)
	syncDownCmd := flag.NewFlagSet("sync-down", flag.ExitOnError)

	if len(os.Args) < 2 {
		fmt.Println("expected 'sync-up' or 'sync-down' subcommands")
		os.Exit(1)
	}

	switch os.Args[1] {
	case "sync-up":
		syncUpCmd.Parse(os.Args[2:])
		syncUp()
	case "sync-down":
		syncDownCmd.Parse(os.Args[2:])
		syncDown()
	default:
		fmt.Println("expected 'sync-up' or 'sync-down' subcommands")
		os.Exit(1)
	}
}

type SyncResponse struct {
	Message   string `json:"message"`
	ProjectId string `json:"project_id"`
}

type SyncConfig struct {
	Bearer    string `json:"bearer"`
	ProjectId string `json:"project_id"`
}

func Poe(err error) {
	if err != nil {
		panic(err)
	}
}

func syncDown() {
	config, err := getConfig()
	Poe(err)

	projectID := config.ProjectId
	println("Syncing down project", projectID)
	syncDownResponse, err := sendSyncDownRequest(projectID, config.Bearer)
	if err != nil {
		fmt.Println("Error during sync-down:", err)
		return
	}

	patchFilePath := "/tmp/last.patch"
	if err := saveResponseToFile(syncDownResponse, patchFilePath); err != nil {
		fmt.Println("Error saving the file:", err)
		return
	}

	fmt.Println("Patch downloaded to:", patchFilePath)

	cmd := exec.Command("patch", "-p4")
	patchFile, err := os.Open("/tmp/last.patch")
	if err != nil {
		panic(err)
	}
	defer patchFile.Close()

	cmd.Stdin = patchFile

	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		fmt.Printf("Output: %s\n", output)
		return
	}

	fmt.Println("Patch applied successfully")
}

func syncUp() {
	projectRootPath := getProjectDir()
	println("Syncing", projectRootPath)
	config, err := getConfig()
	Poe(err)

	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	zipFilePath := "/tmp/gen-code-sync-up-archive.zip"

	err = zipFolder(projectRootPath, zipFilePath)
	if err != nil {
		fmt.Println("Error zipping folder:", err)
		return
	}

	syncUpResponse, err := sendSyncUpRequest(zipFilePath, config.Bearer)
	if err != nil {
		fmt.Println("Error during sync-up:", err)
		return
	}

	err = updateSyncConfigWithProjectId(syncUpResponse.ProjectId)
	Poe(err)

	fmt.Printf("%s, Project ID: %s\n", syncUpResponse.Message, syncUpResponse.ProjectId)
	fmt.Println("Url: http://localhost:7086/app/show-project?id=" + syncUpResponse.ProjectId)

}

func getConfigPath() string {
	configPath := getProjectDir() + "/.coide/sync.json"
	return configPath
}

func updateSyncConfigWithProjectId(projectId string) error {
	config, err := getConfig()
	if err != nil {
		return err
	}

	config.ProjectId = projectId

	updatedContent, err := json.MarshalIndent(config, "", "  ")
	if err != nil {
		return fmt.Errorf("error marshalling updated config: %v", err)
	}

	err = os.WriteFile(getConfigPath(), updatedContent, 0644)
	if err != nil {
		return fmt.Errorf("error writing updated config to file: %v", err)
	}

	return nil
}

func getConfig() (SyncConfig, error) {
	configPath := getConfigPath()
	fileContent, err := os.ReadFile(configPath)
	if err != nil {
		return SyncConfig{}, fmt.Errorf("error reading sync config file: %v", err)
	}
	var config SyncConfig
	err = json.Unmarshal(fileContent, &config)
	if err != nil {
		return SyncConfig{}, fmt.Errorf("error parsing sync config JSON: %v", err)
	}
	return config, err
}

func getProjectDir() string {
	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		panic("Failed to get caller information")
	}
	baseDir := filepath.Dir(filename)
	path := filepath.Join(baseDir, "../..")

	return path
}

func zipFolder(source, destination string) error {
	zipfile, err := os.Create(destination)
	if err != nil {
		return err
	}
	defer zipfile.Close()

	archive := zip.NewWriter(zipfile)
	defer archive.Close()

	// Parse the .gitignore file if it exists
	ignorePatterns, err := parseGitignore(filepath.Join(source, ".gitignore"))
	if err != nil {
		return err
	}
	i := 0

	err = filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if info.IsDir() {
			return nil
		}

		relPath, err := filepath.Rel(source, path)
		if err != nil {
			return err
		}

		relativePath := strings.TrimPrefix(relPath, source)
		// Check if the file matches any patterns in the .gitignore
		if matchesGitignore(relativePath, ignorePatterns) {
			return nil
		}
		i++

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

		writer, err := archive.Create(relPath)
		if err != nil {
			return err
		}

		_, err = io.Copy(writer, file)
		return err
	})
	println("Elements added", i)

	return err
}

func parseGitignore(gitignorePath string) ([]string, error) {
	patterns := []string{
		"/.git/",
	}

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

	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || strings.HasPrefix(line, "#") {
			continue
		}
		patterns = append(patterns, line)
	}

	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return patterns, nil
}

func matchesGitignore(relPath string, patterns []string) bool {
	relPath = "/" + relPath
	for _, pattern := range patterns {
		if match(pattern, relPath) {
			return true
		}
	}
	return false
}

func match(pattern, relPath string) bool {
	isExactMatch := !strings.HasSuffix(pattern, "/")
	isPrefixMatch := strings.HasSuffix(pattern, "/")

	if isExactMatch && pattern == relPath {
		return true
	}
	if isPrefixMatch && strings.HasPrefix(relPath, pattern) {
		return true
	}
	return false
}

func sendSyncUpRequest(zipFilePath, bearerToken string) (*SyncResponse, error) {
	body, contentType, err := createMultipartFormData(zipFilePath, "file[archive]")
	if err != nil {
		return nil, err
	}

	req, err := createRequest("http://localhost:7086/app/perform-sync-up", body, contentType, bearerToken)
	if err != nil {
		return nil, err
	}

	return sendRequest(req)
}

func sendSyncDownRequest(projectID, bearerToken string) (*http.Response, error) {
	body := new(bytes.Buffer)
	writer := multipart.NewWriter(body)

	if err := writer.WriteField("project_id", projectID); err != nil {
		return nil, err
	}
	if err := writer.Close(); err != nil {
		return nil, err
	}

	req, err := createRequest("http://localhost:7086/app/perform-sync-down", body, writer.FormDataContentType(), bearerToken)
	if err != nil {
		return nil, err
	}

	return sendRawRequest(req)
}

func createMultipartFormData(filePath, fieldName string) (*bytes.Buffer, string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return nil, "", err
	}
	defer file.Close()

	body := new(bytes.Buffer)
	writer := multipart.NewWriter(body)

	part, err := writer.CreateFormFile(fieldName, filepath.Base(filePath))
	if err != nil {
		return nil, "", err
	}
	if _, err := io.Copy(part, file); err != nil {
		return nil, "", err
	}

	if err := writer.Close(); err != nil {
		return nil, "", err
	}

	return body, writer.FormDataContentType(), nil
}

func createRequest(url string, body *bytes.Buffer, contentType, bearerToken string) (*http.Request, error) {
	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Authorization", "Bearer "+bearerToken)

	return req, nil
}

func sendRequest(req *http.Request) (*SyncResponse, error) {
	resp, err := sendRawRequest(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	var syncResponse SyncResponse
	if err := json.NewDecoder(resp.Body).Decode(&syncResponse); err != nil {
		return nil, fmt.Errorf("error parsing response: %v", err)
	}

	return &syncResponse, nil
}

func sendRawRequest(req *http.Request) (*http.Response, error) {
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode != http.StatusOK {
		defer resp.Body.Close()
		return nil, fmt.Errorf("bad status: %s", resp.Status)
	}

	return resp, nil
}

func saveResponseToFile(resp *http.Response, filePath string) error {
	out, err := os.Create(filePath)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	return err
}
