using FabAccessAPI.Schema;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace FabAccessAPI
{

    public class MachineException : Exception { }

    /// <summary>
    /// Wraps a capability for accessing the Machines subsystem of BFFH
    /// </summary>
    public class Machines {
        
        private readonly IMachines _machinesCap;

        /// <summary>
        /// Constructs the Wrapper Class from a given capability.
        /// </summary>
        /// <param name="machinesCap">The capability that should be wrapped.</param>
        public Machines(IMachines machinesCap) {
            _machinesCap = machinesCap;
        }

        /// <summary>
        /// List of all machines that BFFH knows about the user has been granted at least read access on
        /// </summary>
        /// <returns>ReadOnlyList of available Machines</returns>
        public async Task<IReadOnlyList<Machine>?> ListMachines() 
        {
            IReadOnlyList<Schema.Machine>? machineList = await _machinesCap.ListMachines().ConfigureAwait(false);
            List<Machine> machineList_new = new List<Machine>();
            foreach(Schema.Machine machine in machineList)
            {
                machineList_new.Add(new Machine(machine));
            }

            return machineList_new;
        }

        /// <summary>
        /// Access a particular machine by known name. This may fail for two reasons:
        /// The user has not been granted access to know the machine exists or the machine does in fact not exist.
        /// In both cases the `machine` result will be a NULL-pointer
        /// </summary>
        /// <param name="name">Name of the Machine</param>
        /// <returns>The Machine we requested</returns>
        public async Task<Machine> GetMachine(string name) {
            var mach = (await _machinesCap.GetMachine(name).ConfigureAwait(false)).Item1;
            if (mach == null) {
                //TODO: Throw a more specific exception!
                throw new MachineException();
            }
            return new Machine(mach);
        }
    }

    /// <summary>
    /// A machine. This represents a machine as BFFH thinks about it which may mean
    ///several machines or just part of a machine in the real world.
    ///By itself this struct is completely useless since it contains only the information
    ///that the machine exists the user is allowed to know about that fact. For all further
    ///information the user has to call the contained capabilities which depending on the
    ///access level may not be set. For example an admin will have every capability here
    ///set but a simple user may only have `read` and `write` set while some users may not
    /// even have `read` set and are unable to even see if the machine is currently in use.
    /// </summary>
    public class Machine {
        private readonly Schema.Machine _machine;

        /// <summary>
        /// Constructs the Wrapper Class from a given capability
        /// </summary>
        /// <param name="machine">The capability that should be wrapped.</param>
        public Machine(Schema.Machine machine) {
            _machine = machine;
        }

        // read operations

        /// <summary>
        /// Get the MInfo Struct for the Machine.
        /// This contains everything BFFH knows about the Machine.
        /// </summary>
        /// <exception cref="UnauthorizedException"></exception>
        /// <returns>The MInfo Struct describing the Machine</returns>
        public async Task<Schema.Machine.MInfo> GetMInfo() {
            var readCap = _machine.Read;
            if (readCap == null) {
                throw new UnauthorizedException();
            }

            return (await _machine.Read.Info().ConfigureAwait(false)).Item1;
        }

        //write operations

        /// <summary>
        /// Try to use a machine. Throws a UnauthorizedException if the user does not have the required
        /// permissions to use this machine.
        ///
        /// Use the Ret() Method of the returned Object to return the machine
        /// </summary>
        /// <exception cref="UnauthorizedException"></exception>
        /// <returns>Capability to give back the machine</returns>
        public Task<Schema.Machine.WriteInterface.IGiveBack> Use() {
            var writeCap = _machine.Write;
            if (writeCap == null) {
                throw new UnauthorizedException();
            }

            return writeCap.Use();
        }

        /// <summary>
        /// Try to get a GiveBack capability for a machine.
        /// </summary>
        /// <returns>Capability to give back the machine or null</returns>
        /// <exception cref="UnauthorizedException"></exception>
        public Task<Schema.Machine.WriteInterface.IGiveBack> GetGiveBack()
        {
            var writeCap = _machine.Write;
            if (writeCap == null)
            {
                throw new UnauthorizedException();
            }

            return writeCap.GetGiveBack();
        }

        /// <summary>
        /// Try to reserve a machine. Throws a UnauthorizedException if the user does not have the required
        /// permissions to use this machine.
        ///
        /// Use the Ret() Method of the returned Object to return the machine
        /// Use the Use() Nethod of the Machine to use your reserved machine.
        /// </summary>
        /// <exception cref="UnauthorizedException"></exception>
        /// <returns>Capability to give back the machine</returns>
        public Task<Schema.Machine.WriteInterface.IGiveBack> Reserve()
        {
            var writeCap = _machine.Write;
            if (writeCap == null)
            {
                throw new UnauthorizedException();
            }

            return writeCap.Reserve();
        }

       
        // public void GiveBack(Schema.Machine.WriteInterface.IGiveBack cap) {
        //     cap.Ret();
        // }

        //manage operations

        /// <summary>
        /// After a machine has been used by an user with low enough permissions it's
        /// in the 'toCheck' state.  This call then allows more priviledged users to
        /// "check" the machine and move it to the `free` state.
        ///
        /// Calling this method signifies that the machine was checked and in an acceptable state.
        ///  </summary>
        public async void MarkOk() {
            var manageCap = _machine.Manage;
            if (manageCap == null) {
                throw new UnauthorizedException();
            }
            // TODO: Do we really want to check this here?
            if ((await GetMInfo().ConfigureAwait(false)).State == State.toCheck) {
                await _machine.Manage.Ok().ConfigureAwait(false);
            }
        }

        /// <summary>
        /// After a machine has been used by an user with low enough permissions it's
        /// in the 'toCheck' state.  This call then allows more priviledged users to
        /// "check" the machine and move it to the `free` state.
        ///
        /// Calling this method signifies that the machine was checked and in an unacceptable state.
        /// It will most likely be marked as `blocked` and the previous user will somehow  be informed.
        ///  </summary>
        public async void MarkNotOk() {
            var manageCap = _machine.Manage;
            if (manageCap == null) {
                throw new UnauthorizedException();
            }
            // TODO: Do we really want to check this here?
            if ((await GetMInfo().ConfigureAwait(false)).State == State.toCheck) {
                await _machine.Manage.NotOk().ConfigureAwait(false);
            }
        }

        //administrative operations

        /// <summary>
        /// Forcefully set a machine state.
        /// </summary>
        /// <param name="state">The desired machine state.</param>
        public async void ForceSetState(State state) {
            var adminCap = _machine.Admin;
            if (adminCap == null) {
                throw new UnauthorizedException();
            }

            await adminCap.ForceSetState(state).ConfigureAwait(false);
        }

        /// <summary>
        /// Set the given user as current responsible
        /// </summary>
        /// <param name="user">The user</param>
        public async void ForceSetUser(String user) {
            var adminCap = _machine.Admin;
            if (adminCap == null) {
                throw new UnauthorizedException();
            }

            await adminCap.ForceSetUser(user).ConfigureAwait(false);
        }
    }
}