import cookie from 'cookie';
import { Strategy } from 'passport-local';
import passport from 'passport';
import signature from 'cookie-signature';
import redis from 'redis';
import session from 'express-session';
import memorystore from 'memorystore';
import connectRedis from 'connect-redis';
import * as models from '../lib/models.js';
import logging from '../lib/logging.js';
import { session_store, auth } from "../lib/config.js";
import assert from "assert";

const log = logging.create("lib/auth.js");

const redisClient = redis.createClient();
const MemoryStore = memorystore(session);
const RedisStore = connectRedis(session);

passport.use(new Strategy(
  async (username, password, cb) => {
    let user = await models.User.auth(username, password);
    return user ? cb(null, user) : cb(null, false);
  })
);

passport.serializeUser((user, cb) => {
  cb(null, user.id);
});

passport.deserializeUser(async (id, cb) => {
  try {
    const user = await models.User.first({id});

    if(user) {
      return cb(null, user);
    } else {
      return cb(null, false);
    }
  } catch(error) {
    log.error(error);
    return cb(null, false);
  }
});

export const configure_sessions = () => {
  if(session_store === "redis") {
    log.info("Using redis to store sessions.");
    return new RedisStore({client: redisClient});
  } else if(session_store === "memory") {
    log.warn("Using memory to store sessions. Restarts will force logins.");
    return new MemoryStore({ checkPeriod: 86400000, });
  } else {
    assert(false, `Error, unknown session store ${session_store} in secrets/config.json. Only "redis" and "memory" supported.`);
    return undefined; // shouldn't reach this but shut up eslint
  }
}

export const sessionStore = configure_sessions();

export const cookie_secret = auth.cookie_secret;
export const cookie_domain = auth.cookie_domain;
assert(cookie_domain !== undefined, "secrets/config.json:auth.cookie_domain isn't set.");

export const login = () => {
  return (req, res, next) => {
    passport.authenticate('local', (err, user, info) => {
      if(err) {
        log.error(err, info);
        return res.status(500).json({error: info});
      } else if(!user) {
        // log.debug(`Auth attempt user is ${user.id}.`);
        return res.status(401).json({error: "Authentication required"});
      } else {
        // log.debug(`User exists, attempting login ${user.id}`);

        return req.login(user, (err) => {
          if(err) {
            log.error(err);
            res.status(403).json({error: "Invalid login"});
            // BUG: is this right or is next("route")
            return next(err);
          } else {
            return next();
          }
        });
      }
    })(req, res, next);
  }
}

export const required = () => {
  return (req, res, next) => {
    const user = req.user;
    if(!user) {
      return res.status(401).json({error: "Authentication required"});
      // WARNING: next('route') is NOT needed to abort the chain of calls despite express docs
    } else {
      return next();
    }
  }
}


export const socket = async (socket) => {
  try {
    if(!socket.handshake.headers.cookie) {
      throw new Error("No cookies in socket handshake headers, disconnecting socket.");
    }

    let cookies = cookie.parse(socket.handshake.headers.cookie);
    if(!cookies) {
      throw new Error("No cookies were parsed even though the header exists, disconnecting socket.");
    }

    const connect_sid = cookies['connect.sid'];
    if(!connect_sid) {
      throw new Error("No connect.sid cookie in the list of cookies, disconnecting socket.");
    }

    if(connect_sid.slice(0, 2) !== 's:') {
      throw new Error(`is_authenticate only supports signed cookies. Your cookies don't start with s: they start with ${connect_sid.slice(0, 2)}`);
    }

    let session_id = signature.unsign(connect_sid.slice(2), cookie_secret);
    log.debug(`Session id is ${session_id}`);

    if(!session_id) {
      throw new Error(`Unable to get session_id ${session_id} with session signature.`);
    }

    return new Promise((resolve, reject) => {
      sessionStore.get(session_id, (err, session) => {
        socket.express_session = session;

        if(err) {
          log.error(err);
          reject(err);
        } else if(session && session.passport && session.passport.user !== undefined) {
          socket.user_id = session.passport.user;
          resolve(true);
        } else {
          socket.disconnect();
          delete socket.user;
          delete socket.express_session;
          resolve(false);
        }
      });
    });
  } catch(error) {
    // not logging the error stack trace for now since i'm using the Error as a jump/clean
    log.debug(error.message);
    socket.disconnect();
    delete socket.user;
    delete socket.express_session;
    return false;
  }
}


export const init = (app) => {
  app.use(passport.initialize());
  app.use(passport.session());
}
