module auth.service;

import slf4d;
import handy_http_primitives;
import handy_http_handlers.filtered_handler;

import auth.model;
import auth.data;
import auth.data_impl_fs;

const ubyte[] PASSWORD_HASH_PEPPER = []; // Example pepper for password hashing

User createNewUser(UserRepository repo, string username, string password) {
    import secured.kdf;
    HashedPassword hash = securePassword(password, PASSWORD_HASH_PEPPER);
    return repo.createUser(username, hash.toString());
}

void deleteUser(User user, UserRepository repo) {
    repo.deleteByUsername(user.username);
}

void changePassword(User user, UserRepository repo, string currentPassword, string newPassword) {
    import secured.kdf;
    auto verificationResult = verifyPassword(currentPassword, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER);
    if (verificationResult == VerifyPasswordResult.Failure) {
        throw new HttpStatusException(HttpStatus.FORBIDDEN, "Incorrect password.");
    }
    if (currentPassword == newPassword) {
        throw new HttpStatusException(HttpStatus.BAD_REQUEST, "New password cannot be the same as your current one.");
    }
    HashedPassword newHash = securePassword(newPassword, PASSWORD_HASH_PEPPER);
    repo.updatePasswordHash(User(user.username, newHash.toString()));
}

/**
 * Generates a new token for a user who's logging in with the given credentials.
 * Params:
 *   username = The user's username.
 *   password = The user's password.
 * Returns: A JWT the user may use to authenticate requests to the API.
 */
string generateTokenForLogin(string username, string password) {
    import secured.kdf;
    
    UserRepository userRepo = new FileSystemUserRepository();
    Optional!User optionalUser = userRepo.findByUsername(username);
    if (optionalUser.isNull) {
        throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
    }
    User user = optionalUser.value;
    debugF!"Verifying password for login attempt for user %s."(user.username);
    auto verificationResult = verifyPassword(password, HashedPassword(user.passwordHash), PASSWORD_HASH_PEPPER);
    debugF!"Verification result for login: %s"(verificationResult);
    if (verificationResult == VerifyPasswordResult.Failure) {
        throw new HttpStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials.");
    }
    return generateTokenForUser(user);
}

/**
 * Generates a new token for a user.
 * Params:
 *   user = The user to generate the token for.
 * Returns: A JWT the user may use to authenticate requests to the API.
 */
string generateTokenForUser(in User user) {
    import jwt4d;
    import std.datetime;
    JwtClaims claims = JwtClaims()
        .issuer("finnow")
        .subject(user.username)
        .expiresIn(minutes(30))
        .issuedAtNow();
    return writeJwt(claims, "test");
}

/**
 * A request filter that only permits authenticated requests to be processed.
 */
class AuthenticationFilter : HttpRequestFilter {
    private static const AUTH_METADATA_KEY = "AuthContext";

    void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain fc) {
        Optional!AuthContext optionalAuth = extractAuthContextFromBearerToken(request, response);
        debugF!"Extracted auth context from bearer token: %s"(optionalAuth.value);
        if (!optionalAuth.isNull) {
            debugF!"Request was authenticated for user: %s"(optionalAuth.value.user.username);
            request.contextData[AUTH_METADATA_KEY] = optionalAuth.value;
            fc.doFilter(request, response); // Only continue the filter chain if a valid auth context was obtained.
            debug_("Filter chain was called.");
        }
    }
}

/// Information about the current request's authentication status.
class AuthContext {
    User user;
    this(User user) {
        this.user = user;
    }
}

/**
 * Helper method to get the authentication context from a request context
 * that was previously passed through this filter.
 * Params:
 *   request = The request to get.
 * Returns: The auth context that has been set.
 */
AuthContext getAuthContext(in ServerHttpRequest request) {
    return cast(AuthContext) request.contextData[AuthenticationFilter.AUTH_METADATA_KEY];
}

Optional!AuthContext extractAuthContextFromBearerToken(
    ref ServerHttpRequest request,
    ref ServerHttpResponse response
) {
    import jwt4d;

    const HEADER_NAME = "Authorization";
    if (!(HEADER_NAME in request.headers)) {
        return setUnauthorized(response, "Missing Authorization header.");
    }

    string authorizationHeader = request.headers[HEADER_NAME][0];
    if (authorizationHeader.length < 7 || authorizationHeader[0..7] != "Bearer ") {
        return setUnauthorized(response, "Invalid Authorization header format. Expected bearer token.");
    }

    string rawToken = authorizationHeader[7..$];
    JwtClaims claims;
    try {
        claims = readJwt(rawToken, "test");
    } catch (JwtException e) {
        return setUnauthorized(response, e.message.idup);
    }
    
    UserRepository userRepo = new FileSystemUserRepository();
    Optional!User optionalUser = userRepo.findByUsername(claims.subject);
    if (optionalUser.isNull) {
        return setUnauthorized(response, "Invalid user.");
    }
    return Optional!AuthContext.of(new AuthContext(optionalUser.value));
}

Optional!AuthContext extractAuthContextFromQueryParam(
    ref ServerHttpRequest request,
    ref ServerHttpResponse response
) {
    import jwt4d;

    string rawToken = request.getParamAs!(string)("t");
    JwtClaims claims;
    try {
        claims = readJwt(rawToken, "test");
    } catch (JwtException e) {
        return setUnauthorized(response, e.message.idup);
    }
    
    UserRepository userRepo = new FileSystemUserRepository();
    Optional!User optionalUser = userRepo.findByUsername(claims.subject);
    if (optionalUser.isNull) {
        return setUnauthorized(response, "Invalid user.");
    }
    return Optional!AuthContext.of(new AuthContext(optionalUser.value));
}

private Optional!AuthContext setUnauthorized(ref ServerHttpResponse response, string msg) {
    response.status = HttpStatus.UNAUTHORIZED;
    response.writeBodyString(msg, ContentTypes.TEXT_PLAIN);
    return Optional!AuthContext.empty;
}
