package main

import "core:fmt"
import "core:mem"
import "core:slice"
import "core:strings"
import "core:sync"
import "core:time"

import libgit "lib:database/git/bindings"
import "lib:database/sqlite"
import "lib:net/http/router"

Repo :: struct {
	name:        string,
	description: string,
	head:        Repo_Head,
}

Repo_Head :: struct {
	author:     string,
	summary:    string,
	updated_at: time.Time,
}

get_head_info :: proc(soft_serve_path, repo_name: string) -> (Repo_Head, bool) {
	repo: libgit.Repository

	repo_path := strings.join({soft_serve_path, "repos", repo_name}, "/")
	defer delete(repo_path)
	repo_path_cstr := strings.clone_to_cstring(repo_path)
	defer delete(repo_path_cstr)
	result := libgit.repository_open_bare(&repo, repo_path_cstr)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return {}, false
	}
	defer libgit.repository_free(repo)

	head_ref: libgit.Reference
	result = libgit.repository_head(&head_ref, repo)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return {}, false
	}
	defer libgit.reference_free(head_ref)

	head_obj: libgit.Object
	result = libgit.reference_peel(&head_obj, head_ref, .Commit)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return {}, false
	}
	defer libgit.object_free(head_obj)

	commit := libgit.Commit(head_obj)
	// oid := libgit.commit_id(commit)
	signature := libgit.commit_author(commit)

	return {
			author = strings.clone_from(signature.name),
			summary = strings.clone_from(libgit.commit_summary(commit)),
			updated_at = time.Time{_nsec = signature.sig_when.time * i64(time.Second)},
		},
		true
}

get_repos :: proc(repos: ^[dynamic]Repo, soft_serve_path, user_filter: string) -> bool {
	libgit.libgit2_init()
	defer libgit.libgit2_shutdown()

	db_path := strings.join({soft_serve_path, "soft-serve.db"}, "/")
	defer delete(db_path)

	db := sqlite.open(db_path) or_return
	defer sqlite.close(db)

	user_id: int
	if user_filter != "" {
		// users
		// id|username|admin|password|created_at|updated_at
		// 1|admin|1||2026-01-15 12:34:08|2026-01-15 12:34:08
		// 2|perlw|0||2026-01-15 12:47:02|2026-01-15 12:47:02
		rows := sqlite.query(db, "SELECT id FROM users WHERE username = ?", user_filter) or_return
		defer sqlite.rows_close(rows)
		if sqlite.rows_next(rows) {
			sqlite.rows_scan(rows, &user_id)
		}
	}

	// repos
	// id|name|project_name|description|private|mirror|hidden|user_id|created_at|updated_at
	// 1|valkyr||Go-powered proxy|0|0|0|2|2026-01-15 14:23:52|2026-01-15 14:23:52
	rows: ^sqlite.Rows
	if user_id > 0 {
		rows = sqlite.query(
			db,
			"SELECT name, description FROM repos WHERE user_id = ? AND private = 0 AND hidden = 0",
			user_id,
		) or_return
	} else {
		rows = sqlite.query(db, "SELECT name, description FROM repos WHERE private = 0 AND hidden = 0") or_return
	}
	defer sqlite.rows_close(rows)
	for sqlite.rows_next(rows) {
		name, description: string
		sqlite.rows_scan(rows, &name, &description)

		git_name := strings.join({name, ".git"}, "")
		defer delete(git_name)
		head := get_head_info(soft_serve_path, git_name) or_return
		append(repos, Repo{name = name, description = description, head = head})
	}

	return true
}

Repo_Map :: struct {
	repos:           [dynamic]Repo,
	soft_serve_path: string,
	user_filter:     string,
	updated_at:      time.Time,
	arena:           mem.Arena,
	update_mutex:    sync.RW_Mutex,
}

repo_map_init :: proc(repo_map: ^Repo_Map, soft_serve_path, user_filter: string) {
	mem.arena_init(&repo_map.arena, make([]u8, 10485760))

	context.allocator = mem.arena_allocator(&repo_map.arena)

	repo_map.repos = make([dynamic]Repo, 0, 10)
	repo_map.soft_serve_path = soft_serve_path
	repo_map.user_filter = user_filter
}

repo_map_destroy :: proc(repo_map: ^Repo_Map) {
	context.allocator = mem.arena_allocator(&repo_map.arena)

	repo_map_clear(repo_map)
	delete(repo_map.repos)
	delete(repo_map.arena.data)
}

repo_map_clear :: proc(repo_map: ^Repo_Map) {
	context.allocator = mem.arena_allocator(&repo_map.arena)

	for repo in repo_map.repos {
		delete(repo.name)
		delete(repo.description)
		delete(repo.head.author)
		delete(repo.head.summary)
	}
	clear(&repo_map.repos)
}

repo_map_update :: proc(repo_map: ^Repo_Map) {
	context.allocator = mem.arena_allocator(&repo_map.arena)

	if time.diff(repo_map.updated_at, time.now()) > time.Minute {
		sync.lock(&repo_map.update_mutex)

		repo_map_clear(repo_map)

		if !get_repos(&repo_map.repos, repo_map.soft_serve_path, repo_map.user_filter) {
			fmt.eprintf("get_repos: failed refreshing repo_map")
		} else {
			fmt.printf("#!# refreshed repo map #!#\n")
		}
		repo_map.updated_at = time.now()

		sync.unlock(&repo_map.update_mutex)
	}
}

repo_map_get_repo :: proc(repo_map: ^Repo_Map, repo_name: string) -> (Repo, bool) {
	for repo in repo_map.repos {
		if repo.name == repo_name {
			return repo, true
		}
	}
	return {}, false
}

repo_map_updater :: proc(
	req: router.Request,
	res: ^router.Response,
	ctx: rawptr,
	next: router.Route_Handler,
	next_ctx: rawptr,
) {
	repo_map := (^Repo_Map)(ctx)
	repo_map_update(repo_map)

	sync.shared_lock(&repo_map.update_mutex)
	next(req, res, next_ctx)
	defer sync.shared_unlock(&repo_map.update_mutex)
}

Entry_Type :: enum {
	File,
	Dir,
	Other,
}

Repo_Tree_Entry :: struct {
	type: Entry_Type,
	name: string,
}

get_repo_tree :: proc(
	repo_map: ^Repo_Map,
	repo_name: string,
	ref_name := "",
	base_path := "",
) -> (
	[]Repo_Tree_Entry,
	bool,
) {
	libgit.libgit2_init()
	defer libgit.libgit2_shutdown()

	repo: libgit.Repository

	repo_name_postfixed := strings.join({repo_name, ".git"}, "")
	defer delete(repo_name_postfixed)

	repo_path := strings.join({repo_map.soft_serve_path, "repos", repo_name_postfixed}, "/")
	defer delete(repo_path)

	repo_path_cstr := strings.clone_to_cstring(repo_path)
	defer delete(repo_path_cstr)

	result := libgit.repository_open_bare(&repo, repo_path_cstr)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.repository_free(repo)

	head_obj: libgit.Object
	if ref_name == "" {
		ref: libgit.Reference
		result = libgit.repository_head(&ref, repo)
		if result < .Ok {
			when ODIN_DEBUG {
				err := libgit.error_last()
				fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			}
			return nil, false
		}
		defer libgit.reference_free(ref)

		result = libgit.reference_peel(&head_obj, ref, .Commit)
		if result < .Ok {
			when ODIN_DEBUG {
				err := libgit.error_last()
				fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			}
			return nil, false
		}
	} else if len(ref_name) > 4 && ref_name[0:4] == "refs" {
		ref: libgit.Reference
		ref_name_cstr := strings.clone_to_cstring(ref_name)
		defer delete(ref_name_cstr)
		result = libgit.reference_lookup(&ref, repo, ref_name_cstr)
		if result < .Ok {
			when ODIN_DEBUG {
				err := libgit.error_last()
				fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			}
			return nil, false
		}
		defer libgit.reference_free(ref)

		result = libgit.reference_peel(&head_obj, ref, .Commit)
		if result < .Ok {
			when ODIN_DEBUG {
				err := libgit.error_last()
				fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			}
			return nil, false
		}
	} else {
		hash_cstr := strings.clone_to_cstring(ref_name)
		defer delete(hash_cstr)

		result = libgit.revparse_single(&head_obj, repo, hash_cstr)
		if result < .Ok {
			when ODIN_DEBUG {
				err := libgit.error_last()
				fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			}
			return nil, false
		}
	}
	defer libgit.object_free(head_obj)

	tree: libgit.Tree
	result = libgit.commit_tree(&tree, libgit.Commit(head_obj))
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.tree_free(tree)

	sub_tree: libgit.Tree
	if base_path != "" {
		base_entry: libgit.Tree_Entry
		base_path_cstr := strings.clone_to_cstring(base_path)
		defer delete(base_path_cstr)
		result = libgit.tree_entry_bypath(&base_entry, tree, base_path_cstr)
		if result < .Ok {
			err := libgit.error_last()
			fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			return nil, false
		}
		defer libgit.tree_entry_free(base_entry)

		if libgit.tree_entry_type(base_entry) != .Tree {
			fmt.eprintf("git error: not a tree -> %s\n", base_path)
			return nil, false
		}

		result = libgit.tree_lookup(&sub_tree, repo, libgit.tree_entry_id(base_entry))
		if result < .Ok {
			err := libgit.error_last()
			fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
			return nil, false
		}
	}
	defer if sub_tree != nil {
		libgit.tree_free(sub_tree)
	}

	search_tree := sub_tree if sub_tree != nil else tree
	count := libgit.tree_entrycount(search_tree)
	repo_tree_list := make([]Repo_Tree_Entry, count)
	for i in 0 ..< count {
		entry := libgit.tree_entry_byindex(search_tree, i)

		name := libgit.tree_entry_name(entry)
		type := libgit.tree_entry_type(entry)

		#partial switch type {
		case .Tree:
			repo_tree_list[i].type = .Dir
		case .Blob:
			repo_tree_list[i].type = .File
		case:
			repo_tree_list[i].type = .Other
		}
		repo_tree_list[i].name = strings.clone_from_cstring(name)
	}

	return repo_tree_list, true
}

get_raw_blob :: proc(repo_map: ^Repo_Map, repo_name: string, ref_name := "", filename := "") -> ([]u8, bool) {
	libgit.libgit2_init()
	defer libgit.libgit2_shutdown()

	repo: libgit.Repository

	repo_name_postfixed := strings.join({repo_name, ".git"}, "")
	defer delete(repo_name_postfixed)

	repo_path := strings.join({repo_map.soft_serve_path, "repos", repo_name_postfixed}, "/")
	defer delete(repo_path)

	repo_path_cstr := strings.clone_to_cstring(repo_path)
	defer delete(repo_path_cstr)

	result := libgit.repository_open_bare(&repo, repo_path_cstr)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.repository_free(repo)

	ref: libgit.Reference
	if ref_name == "" {
		result = libgit.repository_head(&ref, repo)
	} else {
		ref_name_cstr := strings.clone_to_cstring(ref_name)
		defer delete(ref_name_cstr)
		result = libgit.reference_lookup(&ref, repo, ref_name_cstr)
	}
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.reference_free(ref)

	head_obj: libgit.Object
	result = libgit.reference_peel(&head_obj, ref, .Commit)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.object_free(head_obj)

	commit := libgit.Commit(head_obj)
	tree: libgit.Tree
	result = libgit.commit_tree(&tree, commit)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.tree_free(tree)

	entry: libgit.Tree_Entry
	filename_cstr := strings.clone_to_cstring(filename)
	defer delete(filename_cstr)
	result = libgit.tree_entry_bypath(&entry, tree, filename_cstr)
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.tree_entry_free(entry)

	if libgit.tree_entry_type(entry) != .Blob {
		fmt.eprintf("git error: not a blob -> %s\n", filename)
		return nil, false
	}

	blob: libgit.Blob
	result = libgit.blob_lookup(&blob, repo, libgit.tree_entry_id(entry))
	if result < .Ok {
		err := libgit.error_last()
		fmt.eprintf("git error: %d -> %s, %d\n", result, err.message, err.klass)
		return nil, false
	}
	defer libgit.blob_free(blob)

	raw_blob := libgit.blob_rawcontent(blob)
	size := libgit.blob_rawsize(blob)
	return slice.clone(slice.bytes_from_ptr(raw_blob, int(size))), true
}