<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 */
namespace seekquarry\yioop\library\wiki;

/**
 * Holds and updates the record of a single issue tracked against a git
 * repository wiki page. An issue record remembers who reported the issue,
 * who it is assigned to, how urgent it is, whether it is still open, and a
 * history of what happened to it and when. This class only shapes that
 * record as plain data; saving the record and reading it back belong to the
 * model layer, and turning ids or timestamps into readable words belongs to
 * the view layer. Every method returns a fresh record rather than changing
 * one in place, so a caller always works with the value it is handed back.
 *
 * @author Chris Pollett
 */
class WikiIssue
{
    /**
     * Marks an issue that still needs attention
     */
    const STATUS_OPEN = "open";
    /**
     * Marks an issue that has been dealt with
     */
    const STATUS_CLOSED = "closed";
    /**
     * Records that a closed issue was actually fixed
     */
    const RESOLUTION_FIXED = "fixed";
    /**
     * Records that a closed issue was decided against rather than fixed
     */
    const RESOLUTION_WONT_FIX = "wont_fix";
    /**
     * Display status of an open issue no one is working on yet
     */
    const DISPLAY_REPORTED = "reported";
    /**
     * Display status of an open issue given to someone to work on
     */
    const DISPLAY_ASSIGNED = "assigned";
    /**
     * Display status of a closed issue that was fixed
     */
    const DISPLAY_FIXED = "marked_fixed";
    /**
     * Display status of a closed issue decided against rather than fixed
     */
    const DISPLAY_WONT_FIX = "marked_wont_fix";
    /**
     * History entry name for when an issue was first reported
     */
    const ACTION_OPENED = "opened";
    /**
     * History entry name for when an issue was given to someone
     */
    const ACTION_ASSIGNED = "assigned";
    /**
     * History entry name for when an issue was closed
     */
    const ACTION_CLOSED = "closed";
    /**
     * History entry name for when a closed issue was opened again
     */
    const ACTION_REOPENED = "reopened";
    /**
     * History entry name for when an issue's urgency was changed
     */
    const ACTION_PRIORITY = "priority";
    /**
     * Least urgent an issue can be
     */
    const PRIORITY_LOW = 1;
    /**
     * Middle urgency, the level a newly reported issue starts at
     */
    const PRIORITY_MEDIUM = 2;
    /**
     * Most urgent an issue can be
     */
    const PRIORITY_HIGH = 3;
    /**
     * Makes the record of a newly reported issue: open, not yet assigned to
     * anyone, at middle urgency because the reporter does not choose one,
     * and carrying its title and the branch and version it was seen on. The
     * first history entry says who reported it and when.
     *
     * @param int $reporter id of the person reporting the issue
     * @param int $time when the issue was reported, as a Unix timestamp
     * @param string $title short summary of the issue
     * @param string $branch name of the branch the issue was seen on
     * @param string $version the version the issue was seen on, either a
     *      release tag name or a commit name standing for the current code
     * @return array the new issue record
     */
    public static function open($reporter, $time, $title, $branch, $version)
    {
        return [
            "reporter" => $reporter,
            "assignee" => 0,
            "priority" => self::PRIORITY_MEDIUM,
            "status" => self::STATUS_OPEN,
            "resolution" => "",
            "title" => $title,
            "branch" => $branch,
            "version" => $version,
            "history" => [[
                "action" => self::ACTION_OPENED,
                "by" => $reporter,
                "time" => $time]]];
    }
    /**
     * Gives an issue to a person to work on and notes the change in the
     * issue's history.
     *
     * @param array $record the issue record to change
     * @param int $assignee id of the person the issue is given to
     * @param int $by id of the person making the change
     * @param int $time when the change was made, as a Unix timestamp
     * @return array the changed issue record
     */
    public static function assign($record, $assignee, $by, $time)
    {
        $record["assignee"] = $assignee;
        $record["history"][] = [
            "action" => self::ACTION_ASSIGNED,
            "by" => $by,
            "to" => $assignee,
            "time" => $time];
        return $record;
    }
    /**
     * Closes an issue, either as one that was fixed or as one that will not
     * be acted on, and notes the change in the issue's history. An
     * unrecognized reason is treated as fixed.
     *
     * @param array $record the issue record to change
     * @param string $resolution how the issue was closed, either fixed or
     *      won't-fix
     * @param int $by id of the person closing the issue
     * @param int $time when the issue was closed, as a Unix timestamp
     * @param string $commit name of the commit that fixed the issue, kept
     *      when there is one
     * @return array the changed issue record
     */
    public static function close($record, $resolution, $by, $time,
        $commit = "")
    {
        $allowed = [self::RESOLUTION_FIXED, self::RESOLUTION_WONT_FIX];
        if (!in_array($resolution, $allowed)) {
            $resolution = self::RESOLUTION_FIXED;
        }
        $record["status"] = self::STATUS_CLOSED;
        $record["resolution"] = $resolution;
        $entry = [
            "action" => self::ACTION_CLOSED,
            "by" => $by,
            "resolution" => $resolution,
            "time" => $time];
        if ($commit !== "") {
            $record["fix_commit"] = $commit;
            $entry["commit"] = $commit;
        }
        $record["history"][] = $entry;
        return $record;
    }
    /**
     * Opens a closed issue again so it can be worked on once more, clearing
     * how it had been closed and noting the change in its history.
     *
     * @param array $record the issue record to change
     * @param int $by id of the person reopening the issue
     * @param int $time when the issue was reopened, as a Unix timestamp
     * @return array the changed issue record
     */
    public static function reopen($record, $by, $time)
    {
        $record["status"] = self::STATUS_OPEN;
        $record["resolution"] = "";
        $record["history"][] = [
            "action" => self::ACTION_REOPENED,
            "by" => $by,
            "time" => $time];
        return $record;
    }
    /**
     * Puts an issue back to the reported state: open, held by no one, and
     * with no closing reason, noting the change in its history. This is what
     * choosing the reported status does when an issue was assigned or closed.
     *
     * @param array $record the issue record to change
     * @param int $by id of the person making the change
     * @param int $time when the change was made, as a Unix timestamp
     * @return array the changed issue record
     */
    public static function report($record, $by, $time)
    {
        $record["status"] = self::STATUS_OPEN;
        $record["resolution"] = "";
        $record["assignee"] = 0;
        $record["history"][] = [
            "action" => self::ACTION_OPENED,
            "by" => $by,
            "time" => $time];
        return $record;
    }
    /**
     * Changes how urgent an issue is and notes the change in its history.
     *
     * @param array $record the issue record to change
     * @param int $priority the new urgency, a larger number meaning more
     *      urgent
     * @param int $by id of the person making the change
     * @param int $time when the change was made, as a Unix timestamp
     * @return array the changed issue record
     */
    public static function setPriority($record, $priority, $by, $time)
    {
        $record["priority"] = $priority;
        $record["history"][] = [
            "action" => self::ACTION_PRIORITY,
            "by" => $by,
            "priority" => $priority,
            "time" => $time];
        return $record;
    }
    /**
     * Works out the single word that best describes where an issue stands,
     * for the status column and the filter: a closed issue reads as marked
     * fixed or marked won't fix by how it was closed, and an open issue reads
     * as assigned when someone holds it or reported when no one does. A
     * reopened issue is open again, so it reads as assigned or reported once
     * more.
     *
     * @param array $record the issue record to weigh
     * @return string one of the four display-status names
     */
    public static function displayStatus($record)
    {
        if (($record["status"] ?? "") === self::STATUS_CLOSED) {
            if (($record["resolution"] ?? "") === self::RESOLUTION_WONT_FIX) {
                return self::DISPLAY_WONT_FIX;
            }
            return self::DISPLAY_FIXED;
        }
        if (!empty($record["assignee"])) {
            return self::DISPLAY_ASSIGNED;
        }
        return self::DISPLAY_REPORTED;
    }
    /**
     * Reads when an issue was first reported. The opening line of an
     * issue's history is the report itself, so its time is the time the
     * issue was raised.
     *
     * @param array $record the issue record
     * @return int when it was reported as a Unix timestamp, 0 if the
     *      record carries no history
     */
    public static function reportedTime($record)
    {
        return (int)($record["history"][0]["time"] ?? 0);
    }
    /**
     * Chooses what to show as who reported an issue. An issue reported by
     * someone with an account shows their username; one reported by
     * somebody without an account carries the handle they typed in when
     * they reported it, and that is shown instead, because there is no
     * account to look a name up from.
     *
     * @param array $record the issue record
     * @param string $username the name of the account that reported it,
     *      empty when there is no such account
     * @return string what to show as the reporter
     */
    public static function reporterName($record, $username)
    {
        if (!empty($record["handle"])) {
            return (string)$record["handle"];
        }
        return (string)$username;
    }
    /**
     * Finds the user answerable for an issue's current status: the person
     * it is assigned to while it is open and assigned, whoever last closed
     * it once it is fixed or won't be fixed, and otherwise the person who
     * reported it. This is the name the issue list shows next to the status
     * so a reader can see at a glance who to look to.
     *
     * @param array $record the issue record
     * @return int the user id answerable for the current status, 0 if none
     */
    public static function statusUser($record)
    {
        if (($record["status"] ?? "") === self::STATUS_CLOSED) {
            $history = $record["history"] ?? [];
            for ($i = count($history) - 1; $i >= 0; $i--) {
                if (($history[$i]["action"] ?? "") ===
                    self::ACTION_CLOSED) {
                    return (int)($history[$i]["by"] ?? 0);
                }
            }
            return (int)($record["reporter"] ?? 0);
        }
        if (!empty($record["assignee"])) {
            return (int)$record["assignee"];
        }
        return (int)($record["reporter"] ?? 0);
    }
}
