#!/usr/bin/python3
import json
import os
import sys
import subprocess
from pathlib import Path

from middlewared.client import Client
from middlewared.service import ValidationErrors, CallError


def path_in_locked_datasets(path: str) -> bool:
    with Client() as c:
        return c.call('pool.dataset.path_in_locked_datasets', path)


def get_configured_user_group(path: str) -> dict:
    with Client() as c:
        return c.call('filesystem.stat', path)


def get_host_path_attachments(path: str) -> set:
    with Client() as c:
        return {
            attachment['type']
            for attachment in c.call('pool.dataset.attachments_with_path', path)
            if attachment['type'].lower() not in ['kubernetes', 'chart releases']
        }


def get_kubernetes_config() -> dict:
    with Client() as c:
        return c.call('kubernetes.config')


def validate_host_path(path: str, schema_name: str, verrors: ValidationErrors) -> None:
    """
    These validations are taken from `FilesystemService._common_perm_path_validate`.
    Including an additional validation that makes sure all the children under
    a path are on same device.
    """
    schema_name += ".migration.chown"
    p = Path(path)
    if not p.is_absolute():
        verrors.add(schema_name, f"Must be an absolute path: {path}")

    if p.is_file():
        verrors.add(schema_name, f"Recursive operations on a file are invalid: {path}")

    if not p.absolute().as_posix().startswith("/mnt/"):
        verrors.add(
            schema_name,
            f"Changes to permissions on paths that are not beneath the directory /mnt are not permitted: {path}"
        )
    elif len(p.resolve().parents) == 2:
        verrors.add(schema_name, f"The specified path is a ZFS pool mountpoint: {path}")

    # Make sure that dataset is not locked
    if path_in_locked_datasets(path):
        verrors.add(schema_name, f"Dataset is locked at path: {path}.")

    # Validate attachments
    if attachments := get_host_path_attachments(path):
        verrors.add(schema_name, f"The path '{path}' is already attached to service(s): {', '.join(attachments)}.")

    # Make sure all the minio's data directory children are on same device.
    device_id = os.stat(path).st_dev
    for root, dirs, files in os.walk(path):
        for child in dirs + files:
            abs_path = os.path.join(root, child)
            if os.stat(abs_path).st_dev != device_id:
                verrors.add(
                    schema_name,
                    (f"All the children of MinIO data directory should be on "
                     f"same device as root: path={abs_path} device={os.stat(abs_path).st_dev}")
                )
                break


def migrate(values: dict) -> dict:
    # minio user / group ID
    uid = gid = 473
    verrors = ValidationErrors()
    k8s_config = get_kubernetes_config()
    if values["appVolumeMounts"]["export"]["hostPathEnabled"]:
        host_path = values["appVolumeMounts"]["export"]["hostPath"]
    else:
        app_dataset = values["appVolumeMounts"]["export"]["datasetName"]
        host_path = os.path.join(
            "/mnt", k8s_config['dataset'], "releases", values["release_name"], "volumes/ix_volumes", app_dataset
        )

    current_config = get_configured_user_group(host_path)
    if current_config["uid"] == uid and current_config["gid"] == gid:
        return values

    validate_host_path(host_path, values['release_name'], verrors)
    verrors.check()
    # chown the host path
    acltool = subprocess.run([
        "/usr/bin/nfs4xdr_winacl",
        "-a", "chown",
        "-O", str(uid), "-G", str(gid),
        "-r",
        "-c", host_path,
        "-p", host_path], check=False, capture_output=True
    )
    if acltool.returncode != 0:
        raise CallError(f"acltool [chown] on path {host_path} failed with error: [{acltool.stderr.decode().strip()}]")

    return values


if __name__ == "__main__":
    if len(sys.argv) != 2:
        exit(1)

    if os.path.exists(sys.argv[1]):
        with open(sys.argv[1], "r") as f:
            print(json.dumps(migrate(json.loads(f.read()))))
