#include "Log.h"
#include "Pledge.h"
#include "memory/MemoryManager.h"
#include "sys/Syscall.h"
#include "thread/Scheduler.h"
#include <bits/atfile.h>
#include <luna/PathParser.h>

Result<u64> sys_unlinkat(Registers*, SyscallArgs args)
{
    int dirfd = (int)args[0];
    auto path = TRY(MemoryManager::strdup_from_user(args[1]));
    int flags = (int)args[2];

    Thread* current = Scheduler::current();
    TRY(check_pledge(current, Promise::p_cpath));

    auto dirname = TRY(PathParser::dirname(path.view()));
    auto basename = TRY(PathParser::basename(path.view()));

    if (basename.view() == ".") return err(EINVAL);

    kinfoln("unlinkat: remove %s from directory %s, dirfd is %d", basename.chars(), dirname.chars(), dirfd);

    auto inode = TRY(current->resolve_atfile(dirfd, dirname, false, false));
    if (!VFS::can_write(inode, current->auth)) return err(EACCES);

    auto child = TRY(inode->find(basename.chars()));
    if (flags == AT_REMOVEDIR && child->type() != VFS::InodeType::Directory) return err(ENOTDIR);

    if (current->auth.euid != 0 && VFS::is_sticky(inode) && current->auth.euid != inode->metadata().uid &&
        current->auth.euid != child->metadata().uid)
        return err(EACCES);

    TRY(inode->remove_entry(basename.chars()));

    return 0;
}

Result<u64> sys_symlinkat(Registers*, SyscallArgs args)
{
    auto target = TRY(MemoryManager::strdup_from_user(args[0]));
    int dirfd = (int)args[1];
    auto linkpath = TRY(MemoryManager::strdup_from_user(args[2]));

    if (target.is_empty()) return err(ENOENT);

    auto* current = Scheduler::current();
    TRY(check_pledge(current, Promise::p_cpath));

    auto parent = TRY(PathParser::dirname(linkpath.view()));

    auto parent_inode = TRY(current->resolve_atfile(dirfd, parent, false, true));

    if (!VFS::can_write(parent_inode, current->auth)) return err(EACCES);

    auto child_name = TRY(PathParser::basename(linkpath.view()));

    TRY(VFS::validate_filename(child_name.view()));

    auto inode = TRY(parent_inode->fs()->create_symlink_inode(target.view()));
    auto metadata = inode->metadata();
    metadata.uid = current->auth.euid;
    metadata.gid = current->auth.egid;
    TRY(inode->set_metadata(metadata));
    TRY(parent_inode->add_entry(inode, child_name.chars()));

    return 0;
}

Result<u64> sys_readlinkat(Registers*, SyscallArgs args)
{
    int dirfd = (int)args[0];
    auto path = TRY(MemoryManager::strdup_from_user(args[1]));
    char* buf = (char*)args[2];
    usize bufsiz = (usize)args[3];

    auto* current = Scheduler::current();
    TRY(check_pledge(current, Promise::p_rpath));

    auto symlink = TRY(current->resolve_atfile(dirfd, path, true, false));

    if (symlink->type() != VFS::InodeType::Symlink) return err(EINVAL);

    auto linkpath = TRY(symlink->readlink());
    check(!linkpath.is_empty());

    usize nread = linkpath.length();
    if (nread > bufsiz) nread = bufsiz;

    if (!MemoryManager::copy_to_user(buf, linkpath.chars(), nread)) return err(EFAULT);

    return nread;
}

Result<u64> sys_linkat(Registers*, SyscallArgs args)
{
    int olddirfd = (int)args[0];
    auto oldpath = TRY(MemoryManager::strdup_from_user(args[1]));
    int newdirfd = (int)args[2];
    auto newpath = TRY(MemoryManager::strdup_from_user(args[3]));
    int flags = (int)args[4];

    auto* current = Scheduler::current();
    TRY(check_pledge(current, Promise::p_cpath));

    auto parent = TRY(PathParser::dirname(newpath.view()));

    // FIXME: Use AT_SYMLINK_FOLLOW.
    auto target = TRY(current->resolve_atfile(olddirfd, oldpath, flags & AT_EMPTY_PATH, false));

    if (target->type() == VFS::InodeType::Directory) return err(EPERM);

    auto parent_inode = TRY(current->resolve_atfile(newdirfd, parent, false, true));

    if (target->fs() != parent_inode->fs()) return err(EXDEV);

    if (!VFS::can_write(parent_inode, current->auth)) return err(EACCES);

    auto child_name = TRY(PathParser::basename(newpath.view()));

    TRY(VFS::validate_filename(child_name.view()));

    TRY(parent_inode->add_entry(target, child_name.chars()));

    return 0;
}