var _ = require('../lib/underscore'),
    auth = require('./auth'),
    caps = require('./caps'),
    config = require('../config'),
    formidable = require('formidable'),
    hooks = require('../hooks'),
    Stream = require('stream'),
    url_parse = require('url').parse,
    util = require('util'),
    winston = require('winston');

var send;
if (config.SERVE_STATIC_FILES)
	send = require('send');

var escape = require('../common').escape_html;
var routes = [];
var resources = [];

var server = require('http').createServer(function (req, resp) {
	var ip = req.connection.remoteAddress;
	var country;
	if (config.TRUST_X_FORWARDED_FOR)
		ip = parse_forwarded_for(req.headers['x-forwarded-for']) || ip;
	if (config.CLOUDFLARE) {
		ip = req.headers['cf-connecting-ip'] || ip;
		country = req.headers['cf-ipcountry'];
	}
	if (!ip) {
		resp.writeHead(500, {'Content-Type': 'text/plain'});
		resp.end("Your IP could not be determined. "
				+ "This server is misconfigured.");
		return;
	}
	req.ident = caps.lookup_ident(ip, country);
	if (req.ident.timeout)
		return timeout(resp);
	if (req.ident.ban)
		return render_500(resp);
	if (req.ident.slow)
		return slow_request(req, resp);
	handle_request(req, resp);
});
exports.server = server;

function handle_request(req, resp) {
	var method = req.method.toLowerCase();
	var parsed = url_parse(req.url, true);
	req.url = parsed.pathname;
	req.query = parsed.query;
	req.cookies = parse_cookie(req.headers.cookie);

	var numRoutes = routes.length;
	for (var i = 0; i < numRoutes; i++) {
		var route = routes[i];
		if (method != route.method)
			continue;
		var m = req.url.match(route.pattern);
		if (m) {
			route.handler(req, resp, m);
			if (config.DEBUG)
				winston.verbose(route.method.toUpperCase() + ' ' + req.url);
			return;
		}
	}

	if (method == 'get' || method == 'head')
		for (var i = 0; i < resources.length; i++)
			if (handle_resource(req, resp, resources[i]))
				return;

	if (config.SERVE_IMAGES) {
		if (require('../imager').serve_image(req, resp))
			return;
	}

	if (config.SERVE_STATIC_FILES) {
		send(req, req.url, {root: 'www/'}).pipe(resp);
		return;
	}
	render_404(resp);
	if (config.DEBUG)
		winston.verbose('404 ' + req.url + ' fallthrough');
}

function handle_resource(req, resp, resource) {
	var m = req.url.match(resource.pattern);
	if (!m)
		return false;
	var args = [req];
	if (resource.headParams)
		args.push(m);
	args.push(resource_second_handler.bind(null, req, resp, resource));

	var cookie = auth.extract_login_cookie(req.cookies);
	if (cookie) {
		auth.check_cookie(cookie, function (err, ident) {
			if (err && !resource.authPassthrough) {
				if (config.DEBUG)
					winston.verbose('DENY ' + req.url + ' ('  + err + ')');
				return forbidden(resp, 'No cookie.');
			}
			else if (!err)
				_.extend(req.ident, ident);
			resource.head.apply(null, args);
		});
	}
	else if (!resource.authPassthrough) {
		if (config.DEBUG)
			winston.verbose('DENY ' + req.url);
		render_404(resp);
	}
	else
		resource.head.apply(null, args);
	return true;
}

function resource_second_handler(req, resp, resource, err, act, arg) {
	var method = req.method.toLowerCase();
	var log = config.DEBUG;
	if (err) {
		if (err == 404) {
			if (log)
				winston.verbose('404 ' + req.url);
			return render_404(resp);
		}
		else if (err != 500)
			winston.error(err);
		else if (log)
			winston.verbose('500 ' + req.url);
		return render_500(resp);
	}
	else if (act == 'ok') {
		if (log)
			winston.verbose(method.toUpperCase() + ' ' + req.url + ' 200');
		if (method == 'head') {
			var headers = (arg && arg.headers) || noCacheHeaders;
			resp.writeHead(200, headers);
			resp.end();
			if (resource.tear_down)
				resource.tear_down.call(arg);
		}
		else {
			if (resource.tear_down) {
				if (!arg)
					arg = {};
				arg.finished = function () {
					resource.tear_down.call(arg);
				};
			}
			resource.get.call(arg, req, resp);
		}
	}
	else if (act == 304) {
		resp.writeHead(304);
		resp.end();
		if (log)
			winston.verbose('304 ' + req.url);
	}
	else if (act == 'redirect' || (act >= 300 && act < 400)) {
		var headers = {Location: arg};
		if (act == 'redirect')
			act = 303;
		if (log)
			winston.verbose(act + ' ' + req.url + ' to ' + arg);
		if (act == 303.1) {
			act = 303;
			headers['X-Robots-Tag'] = 'nofollow';
		}
		resp.writeHead(act, headers);
		resp.end();
	}
	else if (act == 'redirect_js') {
		if (log)
			winston.verbose('303.js ' + req.url + ' to ' + arg);
		if (method == 'head') {
			resp.writeHead(303, {Location: arg});
			resp.end();
		}
		else
			redirect_js(resp, arg);
	}
	else
		throw new Error("Unknown resource handler: " + act);
}

exports.route_get = function (pattern, handler) {
	routes.push({method: 'get', pattern: pattern,
			handler: auth_passthrough.bind(null, handler)});
};

exports.resource = function (pattern, head, get, tear_down) {
	if (head === true)
		head = function (req, cb) { cb(null, 'ok'); };
	var res = {pattern: pattern, head: head, authPassthrough: true};
	res.headParams = (head.length == 3);
	if (get)
		res.get = get;
	if (tear_down)
		res.tear_down = tear_down;
	resources.push(res);
};

exports.resource_auth = function (pattern, head, get, finished) {
	if (head === true)
		head = function (req, cb) { cb(null, 'ok'); };
	var res = {pattern: pattern, head: head, authPassthrough: false};
	res.headParams = (head.length == 3);
	if (get)
		res.get = get;
	if (finished)
		res.finished = finished;
	resources.push(res);
};

function parse_forwarded_for(ff) {
	if (!ff)
		return null;
	var ips = ff.split(',');
	if (!ips.length)
		return null;
	var last = ips[ips.length - 1].trim();
	/* check that it looks like some kind of IPv4/v6 address */
	if (!/^[\da-fA-F.:]{3,45}$/.test(last))
		return null;
	return last;
}
exports.parse_forwarded_for = parse_forwarded_for;

function auth_passthrough(handler, req, resp, params) {
	var cookie = auth.extract_login_cookie(req.cookies);
	if (!cookie) {
		handler(req, resp, params);
		return;
	}

	auth.check_cookie(cookie, function (err, ident) {
		if (!err)
			_.extend(req.ident, ident);
		handler(req, resp, params);
	});
}

exports.route_get_auth = function (pattern, handler) {
	routes.push({method: 'get', pattern: pattern,
			handler: auth_checker.bind(null, handler, false)});
};

function auth_checker(handler, is_post, req, resp, params) {
	if (is_post) {
		var form = new formidable.IncomingForm();
		form.maxFieldsSize = 50 * 1024;
		form.type = 'urlencoded';
		try {
			form.parse(req, function (err, fields) {
				if (err) {
					resp.writeHead(500, noCacheHeaders);
					resp.end(preamble + escape(err));
					return;
				}
				req.body = fields;
				check_it();
			});
		}
		catch (e) {
			winston.error('formidable threw: ' + e);
			return forbidden(resp, 'Bad request.');
		}
	}
	else
		check_it();

	function check_it() {
		cookie = auth.extract_login_cookie(req.cookies);
		if (!cookie)
			return forbidden(resp, 'No cookie.');
		auth.check_cookie(cookie, ack);
	}

	function ack(err, session) {
		if (err)
			return forbidden(resp, err);
		if (is_post && session.csrf != req.body.csrf)
			return forbidden(resp, "Possible CSRF.");
		_.extend(req.ident, session);
		handler(req, resp, params);
	}
}

function forbidden(resp, err) {
	resp.writeHead(401, noCacheHeaders);
	resp.end(preamble + escape(err));
}

exports.route_post = function (pattern, handler) {
	// auth_passthrough conflicts with formidable
	// (by the time the cookie check comes back, formidable can't
	// catch the form data)
	// We don't need the auth here anyway currently thanks to client_id
	routes.push({method: 'post', pattern: pattern, handler: handler});
};

exports.route_post_auth = function (pattern, handler) {
	routes.push({method: 'post', pattern: pattern,
			handler: auth_checker.bind(null, handler, true)});
};

var noCacheHeaders = {'Content-Type': 'text/html; charset=UTF-8',
		'Expires': 'Thu, 01 Jan 1970 00:00:00 GMT',
		'Cache-Control': 'no-cache, no-store',
		'X-Frame-Options': 'sameorigin',
		'X-XSS-Protection': '1',
};
var preamble = '<!doctype html><meta charset=utf-8>';

exports.noCacheHeaders = noCacheHeaders;

exports.notFoundHtml = preamble + '<title>404</title>404';
exports.serverErrorHtml = preamble + '<title>500</title>Server error';

hooks.hook('reloadResources', function (res, cb) {
	exports.notFoundHtml = res.notFoundHtml;
	exports.serverErrorHtml = res.serverErrorHtml;
	cb(null);
});

function render_404(resp) {
	resp.writeHead(404, noCacheHeaders);
	resp.end(exports.notFoundHtml);
};
exports.render_404 = render_404;

function render_500(resp) {
	resp.writeHead(500, noCacheHeaders);
	resp.end(exports.serverErrorHtml);
}
exports.render_500 = render_500;

function slow_request(req, resp) {
	var n = Math.floor(1000 + Math.random() * 500);
	if (Math.random() < 0.1)
		n *= 10;
	setTimeout(function () {
		if (resp.finished)
			return;
		if (resp.socket && resp.socket.destroyed)
			return resp.end();
		handle_request(req, new Debuff(resp));
	}, n);
}

function timeout(resp) {
	var n = Math.random();
	n = Math.round(9000 + n*n*50000);
	setTimeout(function () {
		if (resp.socket && !resp.socket.destroyed)
			resp.socket.destroy();
		resp.end();
	}, n);
}

function redirect(resp, uri, code) {
	var headers = {Location: uri};
	for (var k in noCacheHeaders)
		headers[k] = noCacheHeaders[k];
	resp.writeHead(code || 303, headers);
	resp.end(preamble + '<title>Redirect</title>'
		+ '<a href="' + encodeURI(uri) + '">Proceed</a>.');
}
exports.redirect = redirect;

var redirectJsTmpl = require('fs').readFileSync('tmpl/redirect.html');

function redirect_js(resp, uri) {
	resp.writeHead(200, noCacheHeaders);
	resp.write(preamble + '<title>Redirecting...</title>');
	resp.write('<script>var dest = "' + encodeURI(uri) + '";</script>');
	resp.end(redirectJsTmpl);
}
exports.redirect_js = redirect_js;

exports.dump_server_error = function (resp, err) {
	resp.writeHead(500, noCacheHeaders);
	resp.write(preamble + '<title>Server error</title>\n<pre>');
	resp.write(escape(util.inspect(err)));
	resp.end('</pre>');
};

function parse_cookie(header) {
	var chunks = {};
	(header || '').split(';').forEach(function (part) {
		var bits = part.match(/^([^=]*)=(.*)$/);
		if (bits)
			try {
				chunks[bits[1].trim()] = decodeURIComponent(
						bits[2].trim());
			}
			catch (e) {}
	});
	return chunks;
}
exports.parse_cookie = parse_cookie;

exports.prefers_json = function (accept) {
	/* Not technically correct but whatever */
	var mimes = (accept || '').split(',');
	for (var i = 0; i < mimes.length; i++) {
		if (/json/i.test(mimes[i]))
			return true;
		else if (/(html|xml|plain|image)/i.test(mimes[i]))
			return false;
	}
	return false;
};

function Debuff(stream) {
	Stream.call(this);
	this.out = stream;
	this.buf = [];
	this.timer = 0;
	this.writable = true;
	this.destroyed = false;
	this.closing = false;
	this._flush = this._flush.bind(this);
	this.on_close = this.destroy.bind(this);
	this.on_error = this.on_error.bind(this);
	stream.once('close', this.on_close);
	stream.on('error', this.on_error);
	this.timeout = setTimeout(this.destroy.bind(this), 120*1000);
}
util.inherits(Debuff, Stream);

var D = Debuff.prototype;

D.writeHead = function () {
	if (!this._check())
		return false;
	this.buf.push({_head: [].slice.call(arguments)});
	this._queue();
	return true;
};

D.write = function (data, encoding) {
	if (!this._check())
		return false;
	if (encoding)
		this.buf.push({_enc: encoding, _data: data});
	else
		this.buf.push(data);
	this._queue();
	return true;
};

D.end = function (data, encoding) {
	if (!this._check())
		return;
	if (encoding)
		this.buf.push({_enc: encoding, _data: data});
	else if (data)
		this.buf.push(data);
	this._queue();
	this.closing = true;
	this.cleanEnd = true;
};

D._check = function () {
	if (!this.writable)
		return false;
	if (!this.out.writable) {
		this.destroy();
		return false;
	}
	if (this.out.sock && this.out.sock.destroyed) {
		this.destroy();
		return false;
	}
	return true;
};

D._queue = function () {
	if (this.timer)
		return;
	if (Math.random() < 0.05)
		return;
	var wait = 500 + Math.floor(Math.random() * 5000);
	if (Math.random() < 0.5)
		wait *= 2;
	this.timer = setTimeout(this._flush, wait);
};

D._flush = function () {
	var limit = 500 + Math.floor(Math.random() * 1000);
	if (Math.random() < 0.05)
		limit *= 3;

	var count = 0;
	while (this.out.writable && this.buf.length && count < limit) {
		var o = this.buf.shift();
		if (o._head) {
			this.out.writeHead.apply(this.out, o._head);
			this.statusCode = this.out.statusCode;
			continue;
		}
		var enc;
		if (o._enc && o._data) {
			enc = o.enc;
			o = o._data;
		}
		if (!o.length)
			continue;
		var n = limit - count;
		if (typeof o == 'string' && o.length > n) {
			this.buf.unshift(o.slice(n));
			o = o.slice(0, n);
		}
		count += o.length;
		if (!this.out.write(o, enc))
			break;
	}
	this.timer = 0;
	if (this.out.writable && this.buf.length)
		this._queue();
	else if (this.closing) {
		if (this.cleanEnd) {
			this.out.end();
			this._clean_up();
			this.emit('close');
		}
		else {
			this.destroy();
		}
	}
	else
		this.emit('drain');
};

D.destroy = function () {
	if (this.destroyed)
		return;
	this._clean_up();
	this.cleanEnd = false;
	this.emit('close');
};

D.destroySoon = function () {
	if (!this.timer)
		return this.destroy();
	this.writable = false;
	this.closing = true;
};

D.on_error = function (err) {
	if (!this.destroyed)
		this._clean_up();
	this.cleanEnd = false;
	this.emit('error', err);
};

D._clean_up = function () {
	this.writable = false;
	this.destroyed = true;
	this.closing = false;
	this.out.removeListener('close', this.on_close);
	this.out.removeListener('error', this.on_error);
	if (this.timer) {
		clearTimeout(this.timer);
		this.timer = 0;
	}
	if (this.timeout) {
		clearTimeout(this.timeout);
		this.timeout = 0;
	}
	if (!this.out.finished) {
		this.out.destroy();
	}
};

D.getHeader = function (name) { return this.out.getHeader(name); };
D.setHeader = function (k, v) { this.out.setHeader(k, v); };
D.removeHeader = function (name) { return this.out.removeHeader(name); };
D.addTrailers = function (headers) { this.out.addTrailers(headers); };
