/*
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * Copyright (C) 2017 Red Hat, Inc.
 */

import React, { useState } from 'react';

import { Button } from '@patternfly/react-core/dist/esm/components/Button';
import { Checkbox } from '@patternfly/react-core/dist/esm/components/Checkbox';
import { ExpandableSection, ExpandableSectionVariant } from
    '@patternfly/react-core/dist/esm/components/ExpandableSection';
import { Form, FormGroup, FormSection } from '@patternfly/react-core/dist/esm/components/Form';
import { FormSelect, FormSelectOption } from '@patternfly/react-core/dist/esm/components/FormSelect';
import {
    Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant
} from '@patternfly/react-core/dist/esm/components/Modal';
import { TextInput } from '@patternfly/react-core/dist/esm/components/TextInput';
import { Stack } from '@patternfly/react-core/dist/esm/layouts/Stack';

import cockpit from 'cockpit';
import type { BasicError } from 'cockpit';
import { InlineNotification } from 'cockpit-components-inline-notification';
import type { Dialogs, DialogResult } from 'dialogs';
import { useInit } from 'hooks';
import { etc_group_syntax, etc_passwd_syntax } from 'pam_user_parser';
import type { PamCommon, PasswdUserInfo, EtcGroupInfo } from 'pam_user_parser';
import * as python from "python";
import { superuser } from 'superuser';
import { fmt_to_fragments } from 'utils.tsx';

import { inode_types } from '../common.ts';
import type { FolderFileInfo } from '../common.ts';

import read_selinux_context from './read-selinux.py';

const _ = cockpit.gettext;

const PERMISSION_OPTIONS: Record<number, string> = {
    0: "no-access",
    1: "no-access",
    2: "write-only",
    3: "write-only",
    4: "read-only",
    5: "read-only",
    6: "read-write",
    7: "read-write",
};

const OPTIONS_PERMISSIONS: Record<string, number> = {
    "no-access": 0,
    "write-only": 2,
    "read-only": 4,
    "read-write": 6,
};

// Convert the permissions mode to string based permissions to support
// passing `+X` to chmod which cannot be combined with numeric mode.
// Cockpit wants to pass `+X` for changing a folder and its contents, this only
// makes folders executable and retains the executable bits on a file and
// compared to `+x` does not make every file executable in a directory.
function mode_to_args(mode: number) {
    const offset_map: Record<number, string> = {
        6: 'u',
        3: 'g',
        0: 'o',
    };

    const letter_map: Record<number, string> = {
        4: 'r',
        2: 'w',
        1: 'X',
    };

    const chmod_args = [];
    for (const offset_str of Object.keys(offset_map)) {
        const offset = parseInt(offset_str, 10);
        const single_mode = (mode >> offset) & 0o7;
        let chmod_add = "";
        let chmod_rem = "";

        for (const digit_str of Object.keys(letter_map)) {
            // An object's keys are automatically converted to a string
            const digit = parseInt(digit_str, 10);
            if ((single_mode & digit) === digit) {
                chmod_add += letter_map[digit];
            } else {
                // Removal needs -x not -X
                chmod_rem += letter_map[digit].toLowerCase();
            }
        }

        if (chmod_add.length !== 0) {
            chmod_add = "+" + chmod_add;
        }

        if (chmod_rem.length !== 0) {
            chmod_rem = "-" + chmod_rem;
        }

        chmod_args.push(`${offset_map[offset]}${chmod_add}${chmod_rem}`);
    }

    return chmod_args.join(",");
}

const EditPermissionsModal = ({ dialogResult, items, path } : {
    dialogResult: DialogResult<void>,
    items: FolderFileInfo[],
    path: string,
}) => {
    cockpit.assert(items[0] !== undefined, "passed items cannot be empty");
    const selected = items[0];
    const is_user_equal = items.every((item) => item.user === items[0].user);
    const is_group_equal = items.every((item) => item.group === items[0].group);
    const has_symlinks = items.some((item) => item?.type === 'lnk');

    const [owner, setOwner] = useState(selected.user);
    const [mode, setMode] = useState(selected.mode ?? 0);
    const [group, setGroup] = useState(selected.group);
    const [errorMessage, setErrorMessage] = useState<string | null>(null);
    const [accounts, setAccounts] = useState<PasswdUserInfo[] | null>(null);
    const [groups, setGroups] = useState<EtcGroupInfo[] | null>(null);
    const [isExecutable, setIsExecutable] = useState((mode & 0b001001001) === 0b001001001);
    const [selinuxContext, setSELinuxContext] = useState<string | null>(null);

    const full_path = path + selected.name;
    const executable_file_types = ["code-file", "file"];

    useInit(async () => {
        try {
            const passwd = await cockpit.spawn(["getent", "passwd"], { err: "message" });
            setAccounts(etc_passwd_syntax.parse(passwd));
        } catch (exc) {
            console.error("Cannot obtain users from getent passwd", exc);
        }

        try {
            const group = await cockpit.spawn(["getent", "group"], { err: "message" });
            setGroups(etc_group_syntax.parse(group));
        } catch (exc) {
            console.error("Cannot obtain users from getent group", exc);
        }

        // When editing multiple files determining the common selinux context might not be possible so don't show it.
        if (items.length === 1) {
            try {
                const selinux_context = await python.spawn(read_selinux_context, [full_path]);
                setSELinuxContext(selinux_context);
            } catch (err) {
                const e = err as python.PythonExitStatus;
                if (e.exit_status !== 2)
                    console.error("Cannot obtain SELinux context", err);
            }
        }
    });

    const changeOwner = (owner: string) => {
        setOwner(owner);
        const currentOwner = accounts?.find(a => a.name === owner);
        const currentGroup = groups?.find(g => g.name === group);
        if (currentOwner && currentGroup?.gid !== currentOwner?.gid &&
            !currentGroup?.userlist.includes(currentOwner?.name)) {
            setGroup(groups?.find(g => g.gid === currentOwner.gid)?.name);
        }
    };

    const spawnEncloseFiles = async () => {
        try {
            await cockpit.spawn(["chmod", "-R", mode_to_args(mode), full_path],
                                { superuser: "try", err: "message" });

            await cockpit.spawn(["chown", "-R", owner + ":" + group, full_path],
                                { superuser: "try", err: "message" });

            dialogResult.resolve();
        } catch (err) {
            const e = err as BasicError;
            setErrorMessage(e.message);
        }
    };

    const spawnEditPermissions = async () => {
        const permissionChanged = items.some(item => item.mode !== mode);
        // We only allow editing multiple files with the same owner:group.
        const ownerChanged = owner !== selected.user || group !== selected.group;
        const file_paths = items.map(item => path + item.name);

        try {
            if (permissionChanged)
                await cockpit.spawn(["chmod", mode.toString(8), ...file_paths],
                                    { superuser: "try", err: "message" });

            if (ownerChanged)
                await cockpit.spawn(["chown", "--no-dereference", owner + ":" + group, ...file_paths],
                                    { superuser: "try", err: "message" });

            dialogResult.resolve();
        } catch (err) {
            const e = err as BasicError;
            setErrorMessage(e.message);
        }
    };

    function permissions_options(mode: number) {
        const options = [
            <FormSelectOption
              key="read-write"
              value="read-write"
              label={_("Read and write")}
            />,
            <FormSelectOption
              key="read-only"
              value="read-only"
              label={_("Read-only")}
            />,
            <FormSelectOption
              key="no-access"
              value="no-access"
              label={_("No access")}
            />
        ];

        // Show write-only when such a file exists, but never offer this as a default option.
        if (mode === 2 || mode === 3) {
            options.push(
                <FormSelectOption
                  key="write-only"
                  value="write-only"
                  label={_("Write-only")}
                />
            );
        }

        return options;
    }

    function setPermissions(mask: number, shift: number, option: string) {
        let val = OPTIONS_PERMISSIONS[option];
        if ((selected?.type === 'reg' && isExecutable) || (selected?.type === 'dir' && option !== "no-access")) {
            val += 1;
        }

        setMode((mode & mask) | (val << shift));
    }

    function setExecutableBits(shouldBeExecutable: boolean) {
        setIsExecutable(shouldBeExecutable);

        // Strip / add executable bits
        if (shouldBeExecutable) {
            setMode(mode | 0b001001001);
        } else {
            setMode(mode & ~0b001001001);
        }
    }

    function sortByName(a: PamCommon, b: PamCommon) {
        return a.name.localeCompare(b.name);
    }

    let description;
    if (items.length === 1) {
        description = (selected.type) ? inode_types[selected.type] : _("Missing type");
    } else {
        description = (
            <ExpandableSection
              truncateMaxLines={1}
              variant={ExpandableSectionVariant.truncate}
              toggleTextExpanded={_("Collapse")}
              toggleTextCollapsed={_("Show all files")}
              className="multiple-files-expandable"
            >
                {items.map(itm => itm.name).join(", ")}
            </ExpandableSection>
        );
    }

    return (
        <Modal
          position="top"
          variant={ModalVariant.small}
          isOpen
          onClose={() => dialogResult.resolve()}
        >
            <ModalHeader
              /* Translators: $0 represents a filename */
              title={items.length === 1
                  ? fmt_to_fragments(_("$0 permissions"), <b className="ct-heading-font-weight">{selected.name}</b>)
                  : cockpit.format(_("Permissions for $0 files"), items.length)}
              description={description}
            />
            <ModalBody>
                <Stack>
                    {errorMessage !== null &&
                    <InlineNotification
                      type="danger"
                      text={errorMessage}
                      isInline
                    />}
                    <Form isHorizontal>
                        {superuser.allowed && accounts && groups && is_user_equal && is_group_equal &&
                        <FormSection title={_("Ownership")}>
                            <FormGroup label={_("Owner")} fieldId="edit-permissions-owner">
                                <FormSelect
                                  onChange={(_, val) => changeOwner(val)} id="edit-permissions-owner"
                                  value={owner}
                                >
                                    {accounts?.sort(sortByName).map(a => {
                                        return (
                                            <FormSelectOption
                                              key={a.name} label={a.name}
                                              value={a.name}
                                            />
                                        );
                                    })}
                                </FormSelect>
                            </FormGroup>
                            <FormGroup label={_("Group")} fieldId="edit-permissions-group">
                                <FormSelect
                                  onChange={(_, val) => setGroup(val)} id="edit-permissions-group"
                                  value={group}
                                >
                                    {groups?.sort(sortByName).map(g => {
                                        return (
                                            <FormSelectOption
                                              key={g.name} label={g.name}
                                              value={g.name}
                                            />
                                        );
                                    })}
                                </FormSelect>
                            </FormGroup>
                        </FormSection>}
                        {!has_symlinks &&
                        <FormSection title={_("Access")}>
                            <FormGroup
                              label={_("Owner access")}
                              fieldId="edit-permissions-owner-access"
                            >
                                <FormSelect
                                  value={PERMISSION_OPTIONS[(mode >> 6) & 7]}
                                  onChange={(_, val) => { setPermissions(0o077, 6, val) }}
                                  id="edit-permissions-owner-access"
                                >
                                    {permissions_options((mode >> 6) & 7)}
                                </FormSelect>
                            </FormGroup>
                            <FormGroup
                              label={_("Group access")}
                              fieldId="edit-permissions-group-access"
                            >
                                <FormSelect
                                  value={PERMISSION_OPTIONS[(mode >> 3) & 7]}
                                  onChange={(_, val) => { setPermissions(0o707, 3, val) }}
                                  id="edit-permissions-group-access"
                                >
                                    {permissions_options((mode >> 3) & 7)}
                                </FormSelect>
                            </FormGroup>
                            <FormGroup
                              label={_("Others access")}
                              fieldId="edit-permissions-other-access"
                            >
                                <FormSelect
                                  value={PERMISSION_OPTIONS[(mode) & 7]}
                                  onChange={(_, val) => { setPermissions(0o770, 0, val) }}
                                  id="edit-permissions-other-access"
                                >
                                    {permissions_options(mode & 7)}
                                </FormSelect>
                            </FormGroup>
                            {selinuxContext !== null &&
                            <FormGroup
                              label={_("Security context")}
                            >
                                <TextInput
                                  id="selinux-context"
                                  value={selinuxContext}
                                  readOnlyVariant="plain"
                                />
                            </FormGroup>}
                        </FormSection>}
                        {selected.type === "reg" &&
                            executable_file_types.includes(selected?.category?.class || "file") &&
                            <Checkbox
                              id="is-executable"
                              label={_("Set executable as program")}
                              isChecked={isExecutable}
                              onChange={() => setExecutableBits(!isExecutable)}
                            />}
                    </Form>
                </Stack>
            </ModalBody>
            <ModalFooter>
                <Button
                  variant="primary"
                  onClick={() => spawnEditPermissions()}
                >
                    {_("Change")}
                </Button>
                {selected.type === "dir" &&
                <Button
                  variant="secondary"
                  onClick={() => spawnEncloseFiles()}
                >
                    {_("Change permissions for enclosed files")}
                </Button>}
                <Button variant="link" onClick={() => dialogResult.resolve()}>{_("Cancel")}</Button>
            </ModalFooter>
        </Modal>
    );
};

export function edit_permissions(dialogs: Dialogs, items: FolderFileInfo[], path: string) {
    dialogs.run(EditPermissionsModal, { items, path });
}
