package leigh.cloudbox.cisco;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import leigh.mecha.cred.Credential;
import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import net.sf.expectit.Expect;
import net.sf.expectit.ExpectBuilder;

import java.io.IOException;
import java.util.Properties;

import static net.sf.expectit.matcher.Matchers.contains;

/**
 * This class provides support for remote execution and configuration of Cisco series routers.
 *
 * @author Alex Leigh
 */
public class CiscoSession implements AutoCloseable {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(CiscoSession.class);
    private final JSch jSch = new JSch();
    private final Session session;
    private final Channel channel;
    private final String enablePw;
    private final Expect expect;
    private String hostname;
    private boolean isEnabled = false;
    private boolean isConfig = false;

    public void setTunnelDestination(String iface, String dest) throws IOException {
        enable();
        config();
        expect.sendLine("int " + iface);
        expect.expect(contains("(config-if)#"));
        expect.sendLine("tunnel destination " + dest);
        expect.expect(contains("#"));
        // TODO Exit & wr mem
    }

    /**
     * Create a new controller. This will immediately connect to the router, so the caller should not delay in
     * calling the accessory methods in order to operate on the router. <code>close()</code> should be called in
     * call cases when the caller is done with this object.
     */
    public CiscoSession(String host, Credential cred, String enablePw) throws Exception {
        session = jSch.getSession(cred.getUsername(), host);
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        session.setPassword(cred.getPassword());
        session.connect();
        channel = session.openChannel("shell");
        this.enablePw = enablePw;

        expect = new ExpectBuilder()
                .withOutput(channel.getOutputStream())
                .withInputs(channel.getInputStream(), channel.getExtInputStream())
                .withEchoOutput(System.out)
                .withEchoInput(System.err)
                .withExceptionOnFailure()
                .build();
        channel.connect();

        loginAndHostname();
    }

    /**
     * Given a raw response, determine the hostname of the router. This will be used to more accurately parse
     * further responses from it.
     */
    public String hostname(String prompt) {
        String clean = prompt.strip();
        return clean.replace(">", "");
    }

    /**
     * Login to the router and setup the initial conditions. This also gets the routers hostname, which will be
     * used later to build expectation.
     */
    private void loginAndHostname() throws IOException {
        String prompt = expect.expect(contains(">")).getInput();
        hostname = hostname(prompt);
        expect.sendLine("terminal length 0");
        expect.expect(contains(expectedPrompt()));
    }

    /**
     * Raise the session enable level. This requires that <code>loginAndHostname()</code> has previously been
     * called. If the session is already enabled, this method does nothing. Once enabled, there is no support
     * for leaving the enable condition. Sending the command to exit will leave the <code>isEnabled</code>
     * flag in the wrong state.
     */
    private void enable() throws IOException {
        if (isEnabled) return;

        expect.sendLine("enable");
        expect.expect(contains("Password:"));
        expect.sendLine(enablePw);
        expect.expect(contains(hostname + "#"));
        isEnabled = true;
    }

    /**
     * Enter configure mode on the remote router. If the router is already in configure mode, this method
     * does nothing.
     */
    private void config() throws IOException {
        expect.sendLine("conf t");
        // We have to manually work it out here, because we don't want to set the config flag in the event of an
        // error condition on the remote router.
        String p = isEnabled ? "#" : ">";
        expect.expect(contains(hostname + "(config)" + p));
        isConfig = true;
    }

    private void endConfigure() throws IOException {
        expect.sendLine("exit");
        String p = isEnabled ? "#" : ">";
        expect.expect(contains(hostname + p));
        isConfig = false;
    }

    /**
     * Return the current expected prompt given the state of the remote router - enabled or not, configured
     * or not. This method works better if the hostname is known from a previous call to
     * <code>loginAndHostname()</code>, but if the hostanme is not know, simple prompts will be returned,
     * such as <code>></code> and <code>#</code>.
     */
    public String expectedPrompt() {
        // tbh I don't even know if you can config outside of enable, but I don't want to be the guy who
        // wrote the bug when turns out one ark night that it is possible.

        String h = hostname;
        if (h == null) {
            h = "";
        }

        if (!isConfig) {
            if (!isEnabled) {
                return h + ">";
            } else {
                return h + "#";
            }
        } else {
            if (!isEnabled) {
                return h + "(config)>";
            } else {
                return h + "(config)#";
            }
        }
    }

    @Override
    public void close() throws Exception {
        logger.info("close() called.");
        expect.close();
        session.disconnect();
        channel.disconnect();
    }
}
