<?php
# Note that www group needs write access to the upload dir
$GLOBALS['uploads_dir'] = '/htdocs/famsite/uploads/';
$GLOBALS['db'] = new PDO('sqlite:/data/famsite.db');
$GLOBALS['user_id'] = null;

# Make a DB query with optional bound parameters.
# Returns a PDOStatement object with these methods:
#   fetch()
#   fetchAll()
#   rowCount() -- ONLY for rows affected by delete, insert, update
#
function dbq($query, $params=null, $debug=false) {
    $sth = $GLOBALS['db']->prepare($query);

    # This will avoid having both field names _and_ numeric indices.
    $sth->setFetchMode(PDO::FETCH_ASSOC);

    $result = $sth->execute($params);

    if($debug){
            $sth->debugDumpParams();
    }

    if(!$result){
        error_log("ERROR: Unable to execute query: "
            . $sth->errorCode
            . " " . print_r($sth->errorInfo(), true));
        error(500, "Fam-Error logged (DBQ).");
    }

    return $sth;
}

# Returns user record OR redirects to not-logged-in message page.
function require_user() {
    if(!isset($_COOKIE['login'])) {
        header('Location: not-logged-in.html');
        exit;
    }

    $login = $_COOKIE['login'];

    $q = dbq('select id, name from users where login = :login',
        ['login'=>$login]);
    $row = $q->fetch();

    if(!$row){
        # Not really sure what happened here because the login cookie
        # *was* set, but it didn't match anything in the DB.
        header('Location: not-logged-in.html?weird_login');
        exit;
    }

    $GLOBALS['user_id'] = $row['id'];

    # Return user record
    return $row;
}

function get_simple_user_list() {
    $users = [];
    $q = dbq('select id, name from users');
    $users_list = $q->fetchAll();
    foreach($users_list as &$u){
        $users[$u['id']] = $u;
    }
    return $users;
}


# Posts
# ===================================================================

$routes = [];

# Fetch Posts
# Re: Storing the "posted" and "updated" stamps as UTC, see
# https://ratfactor.com/cards/sqlite-today-date
# Particularly, "What, even, is a day" on why I'm not storing
# timezones with these.
$routes['GET posts'] = function ($params = []) {
    $q = "SELECT p.rowid,
                 p.user,
                 p.txt,
                 p.posted,
                 p.updated,
                 p.touched,
                 p.filename,
                 (SELECT json_group_array(
                        json_object(
                            'rowid', r.rowid,
                            'user', r.user,
                            'emoji', r.emoji,
                            'txt', r.txt
                        ))
                    FROM reactions r
                   WHERE r.post = p.rowid
                 ) as reactions
            FROM posts p
       LEFT JOIN reactions r ON p.rowid = r.post
         ?WHERE?
        GROUP BY p.rowid
";

    # For testing front-end: delay response
    if(isset($params['sleep']) && $params['sleep'] < 10){
        sleep($params['sleep']);
    }

    # Request is for a specific post by rowid
    if(isset($params['rowid'])){
        $q = str_replace('?WHERE?', 'WHERE p.rowid = :rowid', $q);
        $post = dbq($q, [ 'rowid' => $params['rowid'] ]);
        return [$post->fetch()];
    }

    # Request is for new posts after a specific post 'touched' value,
    # which will give us any posts with edits, reactions, etc.
    if(isset($params['after'])){
        $q = str_replace('?WHERE?', 'WHERE p.touched > :after', $q);
        $posts = dbq($q, [ 'after' => $params['after'] ]);
        return $posts->fetchAll();
    }

    # Default: Return some recent posts
    $q = str_replace('?WHERE?', '', $q);
    $posts = dbq("$q ORDER BY p.rowid DESC LIMIT 30");
    return $posts->fetchAll();
};

# New Post
$routes['POST posts'] = function ($params){
    global $routes;

    if(!is_numeric($params['parent'])){
        $params['parent'] = null;
    }

    $dest_fname = null;

    # If there was an image file attached, move it to final location
    if($_FILES['image']['size'] > 0){
        # Sanitize filename - alphanumeric only and prepend timestamp
        $upload_fname = basename($_FILES['image']['name']);
        $dest_fname = time() . '_' . preg_replace('[^a-zA-Z\d]', '', $upload_fname);

        # Now save image (and thumbnail with 'tn_' prefix)
        save_file_upload($_FILES['image'], $dest_fname);
        save_file_upload($_FILES['thumb'], "tn_$dest_fname");
    }

    # Get a new highest value for 'touched' field. Coalesce only needed on the
    # very first post, but maybe this is the first post!
    $touched = 'COALESCE((SELECT touched FROM posts ORDER BY touched DESC LIMIT 1), 0) + 1';

    $query = "INSERT INTO posts (user, txt, parent, posted, touched, filename)
              VALUES (:user, :txt, :parent, unixepoch('now'), $touched, :filename)
              RETURNING rowid";
    $result = dbq($query, [
        'user'     => $GLOBALS['user_id'],
        'txt'      => $params['txt'],
        'parent'   => $params['parent'],
        'filename' => $dest_fname,
    ]);

    $post_rowid = $result->fetch()['rowid'];

    # Return the post we just inserted so it can be rendered
    return $routes['GET posts'](['rowid' => $post_rowid]);
};

# Update Post
# Note the singular "post" here instead of "posts"
$routes['POST post'] = function ($params){
    global $routes;

    if(!is_numeric($params['rowid'])){
        error(400, "Can't edit a post without an id ('rowid')!");
    }

    # New highest value for 'touched' field
    $touched = '(SELECT touched FROM posts ORDER BY touched DESC LIMIT 1) + 1';

    $query = "UPDATE posts SET txt=:txt, updated=unixepoch('now'), touched=$touched
              WHERE rowid=:rowid";
    $result = dbq($query, [
        'txt'      => $params['txt'],
        'rowid'    => $params['rowid'],
    ]);

    # Return the post we just inserted so it can be rendered
    return $routes['GET posts'](['rowid' => $params['rowid']]);
};

# Delete Post
$routes['DELETE post'] = function ($params){
    global $routes;

    if(!is_numeric($params['rowid'])){
        error(400, "Can't delete a post without an id ('rowid')!");
    }

    $rowid = $params['rowid'];

    $file_msg = "";

    # Does it have an attached file? Delete it.
    $the_post = $routes['GET posts'](['rowid' => $rowid]);
    if(!is_null($the_post[0]['filename'])){
        $file_msg = " (with file)";
        $doomed_file = "{$GLOBALS['uploads_dir']}{$the_post[0]['filename']}";
        unlink($doomed_file);
    }

    # Delete any reactions to post
    $query = "DELETE FROM reactions WHERE post=:rowid";
    $result = dbq($query, ['rowid'    => $rowid]);

    # Delete post
    $query = "DELETE FROM posts WHERE rowid=:rowid";
    $result = dbq($query, ['rowid'    => $rowid]);

    return "Post $rowid deleted$file_msg.";
};

# Add reaction
$routes['POST reaction'] = function ($params){
    global $routes;

    $query = "INSERT INTO reactions (user, post, emoji, txt)
              VALUES (:user, :post, :emoji, :txt)";
    $result = dbq($query, [
        'user'  => $GLOBALS['user_id'],
        'post'  => $params['post'], # fkey to posts.rowid
        'emoji' => $params['emoji'],
        'txt'   => $params['txt'],
    ]);

    # Update post also with a new highest value for 'touched' field
    $touched = '(SELECT touched FROM posts ORDER BY touched DESC LIMIT 1) + 1';
    $query = "UPDATE posts SET touched=$touched WHERE rowid=:post";
    $result = dbq($query, ['post' => $params['post']]);

    # Return the whole post we reacted to so it can be re-rendered
    return $routes['GET posts'](['rowid' => $params['post']]);
};


function save_file_upload($input_file, $dest_fname){
    $dest_path = $GLOBALS['uploads_dir'] . $dest_fname;

    if(!move_uploaded_file($input_file['tmp_name'], $dest_path)){
        error(500, 'Failed to handle file upload.');
    }
}

class MyHttpErrorResponse extends Exception {}
function error($code, $msg){
    throw new MyHttpErrorResponse($msg, $code);
}

function get_exception(){
    error(400, 'bad request - which you asked for!');
}

# =========================================================
# If this page was called directly, it's an API request.

if(str_contains($_SERVER['REQUEST_URI'], 'fam.php')){
    require_user();
    $method = $_SERVER['REQUEST_METHOD'];
    $resource = $_REQUEST['r'];
    $fn = "{$method} {$resource}";
    if(!is_callable($routes[$fn])){
        http_response_code(404);
        echo "No such {$method} method for resource '{$resource}'";
        exit;
    }

    try{
        $result = $routes[$fn]($_REQUEST);
        echo json_encode($result);
        exit; # return with HTTP 200 OK
    } catch (MyHttpErrorResponse $e){
        error_log(print_r($e,true));

    } catch (Exception $e){
        error_log("Fam-Error for route '$fn':");
        error_log($e->getMessage() . "\n");
        // Have to do our own http_response_code() here
        //  MyHttpErrorResponse exception caught above!
        http_response_code(500);
        echo "Error logged for: $fn";
        exit;
    }
}
