const {
  winPath,
  createDebug,
  glob,
  parseRequireDeps,
} = require('@umijs/utils');
const path = require('path');
const fs = require('fs');

const assert = require('assert');
const bodyParser = require('body-parser');
const pathToRegexp = require('path-to-regexp');
const multer = require('multer');
const VALID_METHODS = ['get', 'post', 'put', 'patch', 'delete'];
const BODY_PARSED_METHODS = ['post', 'put', 'patch', 'delete'];
const debug = createDebug('preset-build-in:mock:utils');
const registerBabel = paths => {
  // support
  // clear require cache and set babel register
  const requireDeps = paths.reduce((memo, file) => {
    memo = memo.concat(parseRequireDeps(file));
    return memo;
  }, []);
  requireDeps.forEach(f => {
    if (require.cache[f]) {
      delete require.cache[f];
    }
  });
};

const getMockData = ({ cwd, ignore = [], registerBabel = () => {} }) => {
  const mockPaths = [
    ...(glob.sync('mock/**/*.[jt]s', {
      cwd,
      ignore,
    }) || []),
    ...(glob.sync('**/_mock.[jt]s', {
      cwd,
      ignore,
    }) || []),
  ]
  .map((p) => path.join(cwd, p))
  .filter((p) => p && fs.existsSync(p))
  .map((p) => winPath(p));

  debug(`load mock data including files ${JSON.stringify(mockPaths)}`);

  registerBabel(mockPaths);

  // get mock data
  const mockData = normalizeConfig(getMockConfig(mockPaths));

  const mockWatcherPaths = [...(mockPaths || []), path.join(cwd, 'mock')]
  .filter((p) => p && fs.existsSync(p))
  .map((p) => winPath(p));

  return {
    mockData,
    mockPaths,
    mockWatcherPaths,
  };
};

function parseKey(key) {
  let method = 'get';
  // eslint-disable-next-line no-shadow
  let path = key;
  if (/\s+/.test(key)) {
    const splited = key.split(/\s+/);
    method = splited[0].toLowerCase();
    // eslint-disable-next-line prefer-destructuring
    path = splited[1];
  }
  assert(
    VALID_METHODS.includes(method),
    `Invalid method ${method} for path ${path}, please check your mock files.`,
  );
  return {
    method,
    path,
  };
}
function getMockConfig(files) {
  return files.reduce((memo, mockFile) => {
    try {
      const m = require(mockFile); // eslint-disable-line
      memo = {
        ...memo,
        ...(m.default || m),
      };
      return memo;
    } catch (e) {
      throw new Error(e.stack);
    }
  }, {});
}
function normalizeConfig(config) {
  return Object.keys(config).reduce((memo, key) => {
    const handler = config[key];
    const type = typeof handler;
    assert(
      type === 'function' || type === 'object',
      `mock value of ${key} should be function or object, but got ${type}`,
    );
    // eslint-disable-next-line no-shadow
    const { method, path } = parseKey(key);
    const keys = [];
    const re = pathToRegexp(path, keys);
    memo.push({
      method,
      path,
      re,
      handler: createHandler(method, path, handler),
    });
    return memo;
  }, []);
}

// eslint-disable-next-line no-shadow
function createHandler(method, path, handler) {
  return function(req, res, next) {
    if (BODY_PARSED_METHODS.includes(method)) {
      bodyParser.json({ limit: '5mb', strict: false })(req, res, () => {
        bodyParser.urlencoded({ limit: '5mb', extended: true })(
          req,
          res,
          () => {
            sendData();
          },
        );
      });
    } else {
      sendData();
    }

    function sendData() {
      if (typeof handler === 'function') {
        multer().any()(req, res, () => {
          handler(req, res, next);
        });
      } else {
        res.json(handler);
      }
    }
  };
}

function decodeParam(val) {
  if (typeof val !== 'string' || val.length === 0) {
    return val;
  }
  try {
    return decodeURIComponent(val);
  } catch (err) {
    if (err instanceof URIError) {
      err.message = `Failed to decode param ' ${val} '`;
      // @ts-ignore
      err.status = 400;
      // @ts-ignore
      err.statusCode = 400;
    }
    throw err;
  }
}

function cleanRequireCache(paths) {
  Object.keys(require.cache).forEach(file => {
    if (paths.some(p => winPath(file).indexOf(p) > -1)) {
      delete require.cache[file];
    }
  });
}

function matchMock(req, mockData) {
  const { method } = req;
  const targetMethod = method.toLowerCase();
  // eslint-disable-next-line no-underscore-dangle
  const resolvepath = req._parsedUrl.path;
  for (const mock of mockData) {
    // eslint-disable-next-line no-shadow
    const { method, re, keys } = mock;
    if (method === targetMethod) {
      // eslint-disable-next-line no-underscore-dangle
      const match = re.exec(resolvepath);
      if (match) {
        const params = {};
        for (let i = 1; i < match.length; i += 1) {
          const key = keys[i - 1];
          const prop = key.name;
          const val = decodeParam(match[i]);
          // @ts-ignore
          // eslint-disable-next-line no-undef
          if (val !== undefined || !hasOwnProdperty.call(params, prop)) {
            params[prop] = val;
          }
        }
        req.params = params;
        return mock;
      }
    }
  }
}

function getFlatRoutes(routes) {
  return routes.reduce((memo, route) => {
    // eslint-disable-next-line no-shadow
    const { routes, path } = route;
    if (path && !path.includes('?')) {
      memo.push(route);
    }
    if (routes) {
      memo = memo.concat(
        getFlatRoutes({
          routes,
        }),
      );
    }
    return memo;
  }, []);
}

function getConflictPaths({ mockData, routes }) {
  const conflictPaths = [];
  getFlatRoutes({ routes }).forEach(route => {
    const { path, redirect } = route;
    if (path && !path.startsWith(':') && !redirect) {
      const req = {
        path: !path.startsWith('/') ? `/${path}` : path,
        method: 'get',
      };
      const matched = matchMock(req, mockData);
      if (matched) {
        conflictPaths.push({ path: matched.path });
      }
    }
  });
  return conflictPaths;
}
module.exports = {
  getMockData,
  matchMock,
  cleanRequireCache,
};
