const EventEmitter = require('events');
const { Organisations } = require('@dougal/organisations');

function randomUUID () {
	return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
		const r = Math.random() * 16 | 0;
		const v = c === 'x' ? r : (r & 0x3 | 0x8);
		return v.toString(16);
	});
}

class User extends EventEmitter {

	// Valid field names
	static fields = [ "ip", "host", "name", "email", "description", "colour", "active", "organisations", "meta" ]

	static validUUID (str) {
		const uuidv4Rx = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
		return uuidv4Rx.test(str);
	}

	static validIPv4 (str) {
		const ipv4Rx = /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[1-2][0-9]|3[0-2]))?$/;
		return ipv4Rx.test(str);
	}

	static validIPv6 (str) {
		const ipv6Rx = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}|:((?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?))|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|(2[0-4][0-9]|[01]?[0-9][0-9]?))))$/;
		return ipv6Rx.test(str);
	}

	static validHostname (str) {
		const hostnameRx = /^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,}$/;
		return hostnameRx.test(str);
	}

	#setString (k, v) {
		if (typeof v === "undefined") {
			this.values[k] = v;
		} else {
			this.values[k] = String(v).trim();
		}
		this.emit("changed", k, v);
		this.#updateTimestamp();
	}

	#updateTimestamp (v) {
		if (typeof v === "undefined") {
			this.#timestamp = (new Date()).valueOf();
		} else {
			this.#timestamp = (new Date(v)).valueOf();
		}
		this.emit("last_modified", this.#timestamp);
	}

	// Create a new instance of `other`, where `other` is
	// an instance of User or of a derived class
	#clone (other = this) {
		const clone = new this.constructor();
		Object.assign(clone.values, other.values);
		clone.organisations = new Organisations(other.organisations);
		return clone;
	}

	values = {}

	#timestamp

	constructor (data) {
		super();

		User.fields.forEach( f => this[f] = data?.[f] );
		this.values.id = data?.id ?? randomUUID();
		this.values.active = !!this.active;
		this.values.hash = data?.hash;
		this.values.password = data?.password;
		this.values.organisations = new Organisations(data?.organisations);
		this.#updateTimestamp(data?.last_modified);
	}

	/*
	 * Getters
	 */

	get id () { return this.values.id }

	get ip () { return this.values.ip }

	get host () { return this.values.host }

	get name () { return this.values.name }

	get email () { return this.values.email }

	get description () { return this.values.description }

	get colour () { return this.values.colour }

	get active () { return this.values.active }

	get organisations () { return this.values.organisations }

	get password () { return this.values.password }

	get timestamp () { return new Date(this.#timestamp) }

	/*
	 * Setters
	 */

	set id (v) {
		if (typeof v === "undefined") {
			this.values.id = randomUUID();
		} else if (User.validUUID(v)) {
			this.values.id = v;
		} else {
			throw new Error("Invalid ID format (must be UUIDv4)");
		}
		this.emit("changed", "id", this.values.id);
		this.#updateTimestamp();
	}

	set ip (v) {
		if (User.validIPv4(v) || User.validIPv6(v) || typeof v === "undefined") {
			this.values.ip = v;
		} else {
			throw new Error("Invalid IP address or subnet");
		}
		this.emit("changed", "ip", this.values.ip);
		this.#updateTimestamp();
	}

	set host (v) {
		if (User.validHostname(v) || typeof v === "undefined") {
			this.values.host = v;
		} else {
			throw new Error("Invalid hostname");
		}
		this.emit("changed", "host", this.values.host);
		this.#updateTimestamp();
	}

	set name (v) {
		this.#setString("name", v);
	}

	set email (v) {
		// TODO should validate, buy hey!
		this.#setString("email", v);
	}

	set description (v) {
		this.#setString("description", v);
	}

	set colour (v) {
		this.#setString("colour", v);
	}

	set active (v) {
		this.values.active = !!v;
		this.emit("changed", "active", this.values.active);
		this.#updateTimestamp();
	}

	set organisations (v) {
		this.values.organisations = new Organisations(v);
		this.emit("changed", "organisations", this.values.organisations);
		this.#updateTimestamp();
	}

	set password (v) {
		this.values.password = v;
		this.emit("changed", "password", this.values.password);
		this.#updateTimestamp();
	}


	/*
	 * Validation methods
	 */

	get errors () {
		let err = [];

		if (!this.id) err.push("ERR_NO_ID");
		if (!this.name) err.push("ERR_NO_NAME");
		if (!this.organisations.length) err.push("ERR_NO_ORG");

		return err;
	}

	get isValid () {
		return this.errors.length == 0;
	}

	/*
	 * Filtering methods
	 */

	filter (other) {
		// const filteredUser = new User(this);
		const filteredUser = this.#clone();
		filteredUser.organisations = this.organisations.filter(other.organisations);
		return filteredUser;
	}

	/** Return users that are visible to me.
	 *
	 * These are users with which at leas one common organisation
	 * with read, write or delete access to.
	 *
	 * If we are wildcarded ("*"), we see everyone.
	 *
	 * If a peer is wildcarded, they can be seen by everone.
	 */
	peers (list) {
		if (this.organisations.value("*")) {
			return list;
		} else {
			return list.filter( user => this.canRead(user) );
			// return list.filter( user =>
				// user.organisations.value("*") ||
				// user.organisations.filter(this.organisations).length > 0
				// this.organisations.filter(user.organisations).length > 0
			// );
		}
	}

	/** Return users that I can edit
	 *
	 * These users must belong to an organisation
	 * over which I have edit rights.
	 *
	 * If we are edit wildcarded, we can edit everyone.
	 */
	editablePeers (list) {
		const editableOrgs = this.organisations.accessToOperation("edit");
		if (editableOrgs.value("*")) {
			return list;
		} else {
			return list.filter( user => this.canEdit(user) );
				// editableOrgs.filter(user.organisations).length > 0
			// );
		}
	}

	/*
	 * General methods
	 */

	/** Return `true` if we are `other`
	 */
	is (other) {
		return this.id == other.id;
	}

	canDo (operation, other) {
		if (this.organisations.get('*')?.[operation])
			return true;

		if (other instanceof User) {
			return other.organisations.names().some(name => this.organisations.get(name)?.[operation]);
		} else if (other instanceof Organisations) {
			return other.accessToOperation(operation).names().some(name => this.organisations.get(name)?.[operation]);
		} else if (other?.organisations) {
			return this.canDo(operation, new Organisations(other.organisations));
		} else if (other instanceof Object) {
			return this.canDo(operation, new Organisations(other));
		}
		return false;
	}

	canRead (other) {
		return this.canDo("read", other);
	}

	canWrite (other) {
		return this.canDo("write", other);
	}

	canEdit (other) {
		return this.canDo("edit", other);
	}

	/** Perform an edit on another user
	 *
	 * Syntax: user.edit(other).to(another);
	 *
	 * Applies to `other` the changes described in `another`
	 * that are permitted to `user`. The argument `another`
	 * must be a plain object (not a `User` instance) with
	 * only the properties that are to be changed.
	 *
	 * NOTE: Organisations are not merged, they are overwritten
	 * and then filtered to ensure that the edited user does not
	 * gain more privileges than those granted to the editing
	 * user.
	 *
	 * Example:
	 *
	 * // This causes user test77 to set user x23 to
	 * // inactive
	 * test77.edit(x23).to({active: false})
	 */
	edit (other) {
		if (this.canEdit(other)) {
			return {
				to: (another) => {
					const newUser = Object.assign(this.#clone(other), another);
					return newUser.filter(this);
				}
			}
		}
		// Do not fail or throw but return undefined
	}

	/** Create a new user similar to us except it doesn't have `edit` rights
	 * by default
	 */
	spawn (init = {}, mask = {read: true, write: true, edit: false}) {
		// const user = new User(init);
		const user = this.#clone(init);
		user.organisations = this.organisations.accessToOperation("edit").disableOperation("edit");
		user.organisations.overlord = this.organisations;
		return user;
	}



	/*
	 * Conversion and presentation methods
	 */

	toJSON () {
		return {
			id: this.id,
			ip: this.ip,
			host: this.host,
			name: this.name,
			email: this.email,
			description: this.description,
			colour: this.colour,
			active: this.active,
			organisations: this.organisations.toJSON(),
			password: this.password
		}
	}

	toString (replacer, space) {
		return JSON.stringify(this.toJSON(), replacer, space);
	}

}


if (typeof module !== 'undefined' && module.exports) {
    module.exports = User; // CJS export
}

// ESM export
if (typeof exports !== 'undefined' && !exports.default) {
    exports.default = User; // ESM export
}
