const VersionedModel = require('../VersionedModel')
const { ObjectId } = require('mongodb')
const uuid = require('uuid/v4')

const ActiveScope = require('../scopes/Active.scope')

/*
 * Page Model
 * -------------------------------------------------------------
 * Put some description here!
 */
class Page extends VersionedModel {
	static get services() {
		return [...super.services, 'models'];
	}
	static get schema() {
		// Return a flitter-orm schema here.
		return {
			...super.schema,
			UUID: {type: String, default: () => uuid()},
			Name: String,
			OrgUserId: ObjectId,
			IsPublic: {type: Boolean, default: true},
			IsVisibleInMenu: {type: Boolean, default: true},
			ParentId: String,
			NodeIds: [String],
			CreatedAt: {type: Date, default: () => new Date},
			UpdatedAt: {type: Date, default: () => new Date},
			DeletedAt: Date,
			Active: {type: Boolean, default: true},
			CreatedUserId: {type: String},
			UpdateUserId: {type: String},
			ChildPageIds: [String],

			// Menu flags
			noDelete: { type: Boolean, default: false },
			virtual: { type: Boolean, default: false },

			// Sharing properties
			shared_users_view: [ObjectId],
			shared_users_update: [ObjectId],
			shared_users_manage: [ObjectId],
		};
	}

	static scopes = [new ActiveScope]

	static async visible_by_user(user) {
		const user_root = await user.get_root_page()
		const user_root_children = await user_root.visible_flat_children(user)

		const view_only_trees = await this.find({ shared_users_view: user._id })
		let view_only_children = []
		for ( const tree of view_only_trees ) {
			if ( await tree.is_accessible_by(user) ) {
				view_only_children.push(tree)
				view_only_children = [...view_only_children, await tree.visible_flat_children(user)]
			}
		}

		const update_trees = await this.find({ shared_users_update: user._id })
		let update_children = []
		for ( const tree of update_trees ) {
			if ( await tree.is_accessible_by(user) ) {
				update_children.push(tree)
				update_children = [...update_children, await tree.visible_flat_children(user)]
			}
		}

		const manage_trees = await this.find({ shared_users_manage: user._id })
		let manage_children = []
		for ( const tree of manage_trees ) {
			if ( await tree.is_accessible_by(user) ) {
				manage_children.push(tree)
				manage_children = [...manage_children, await tree.visible_flat_children(user)]
			}
		}

		const all_children = [...user_root_children, ...view_only_children, ...update_children, ...manage_children]
		const unique_children = []
		const seen_UUIDs = []
		all_children.forEach(child => {
			if ( !seen_UUIDs.includes(child.UUID) ) {
				unique_children.push(child)
				seen_UUIDs.push(child.UUID)
			}
		})

		return unique_children
	}

	async visible_flat_children(user) {
		const children = await this.childPages
		let visible = []
		if ( children ) {
			for ( const child of children ) {
				if ( !(await child.is_accessible_by(user)) ) continue
				visible.push(child)
				visible = [...visible, ...(await child.visible_flat_children(user))]
			}
		}

		return visible
	}

	is_shared() {
		return this.shared_users_view.length > 0 || this.shared_users_update.length > 0 || this.shared_users_manage.length > 0
	}

	// ================= RELATIONSHIPS =================
	get user() {
		const User = this.models.get("auth:User")
		return this.belongs_to_one(User, "OrgUserId", "_id")
	}

	get nodes() {
		const Node = this.models.get("api:Node")
		const node_promise = this.has_many(Node, "NodeIds", "UUID")
		if ( this.is_a_version() ) {
			// return the nodes for this version!
			return (async () => {
				const nodes = await node_promise
				const version_data = this.raw_version_data()
				const return_nodes = []
				if ( version_data?.node_version_nums ) {
					const node_x_version_num = {}
					for ( const item of version_data.node_version_nums ) {
						node_x_version_num[item.NodeId] = item.version_num
					}

					for ( const node of nodes ) {
						if ( node_x_version_num[node.UUID] ) {
							return_nodes.push(await node.as_version(node_x_version_num[node.UUID]))
						} else {
							return_nodes.push(node)
						}
					}
				}

				return return_nodes
			})()
		} else {
			return node_promise
		}
	}

	get childPages() {
		const Page = this.models.get("api:Page")
		return this.has_many(Page, "ChildPageIds", "UUID")
	}

	get parent() {
		const Parent = this.models.get("api:Page")
		return this.belongs_to_one(Parent, "ParentId", "UUID")
	}

	get view_users() {
		const User = this.models.get('auth:User')
		return this.has_many(User, 'shared_users_view', '_id')
	}

	get update_users() {
		const User = this.models.get('auth:User')
		return this.has_many(User, 'shared_users_update', '_id')
	}

	get manage_users() {
		const User = this.models.get('auth:User')
		return this.has_many(User, 'shared_users_manage', '_id')
	}

	// ================= SECURITY =================
	accessible_by(user, mode = 'view') {
		const base_access = user.can(`page:${this.UUID}:${mode}`)
	}

	async is_accessible_by(user, mode = 'view') {
		const can_manage = user.can(`page:${this.UUID}:manage`)
		const can_update = user.can(`page:${this.UUID}:update`)
		const can_view = user.can(`page:${this.UUID}:view`)
		const can_all = user.can(`page:${this.UUID}`)

		// Allow universal access
		if ( can_all ) return true
		// deny if blocked
		else if ( user.can(`page:${this.UUID}:block`) ) return false
		// manage, update, view can view
		else if ( mode === 'view' && (can_manage || can_update || can_view) ) return true
		// manage, update can update
		else if ( mode === 'update' ) {
			if ( can_manage || can_update ) return true
			// If other permissions are explicitly set for this page, use those
			else if ( can_view ) return false
		}
		// manage can manage
		else if ( mode === 'manage' ) {
			if ( can_manage ) return true
			// If other permissions are explicitly set for this page, use those
			else if ( can_update || can_view ) return false
		}

		// allow universal access
		// deny blocked users

		// If there are no explicit permissions set for this node, check the parent
		const parent = await this.parent
		if ( parent ) return (await this.parent).is_accessible_by(user, mode)
		else return false
	}

	async access_level_for(user) {
		if ( await this.is_accessible_by(user, 'manage') ) return 'manage'
		else if ( await this.is_accessible_by(user, 'update') ) return 'update'
		else if ( await this.is_accessible_by(user, 'view') ) return 'view'
		else return false
	}

	async share_with(user, level = 'view') {
		if ( !['view', 'update', 'manage'].includes(level) ) {
			throw new Error(`Invalid share level: ${level}`)
		}

		// Remove existing sharing info
		await this.unshare_with(user)

		// Add the page to the user's permissions:
		user.allow(`page:${this.UUID}:${level}`)

		// Add the user to the appropriate access list
		this[`shared_users_${level}`].push(user._id)

		// TODO replace user.uid with name of user when we support that
		await this.version_save(`Shared with ${user.uid} (${level} access)`, user.id)
		await user.save()
	}

	async unshare_with(user) {
		// Remove this page from the user's permissions
		if ( user.can(`page:${this.UUID}`) ) user.disallow(`page:${this.UUID}`)
		for ( const level of ['view', 'update', 'manage'] ) {
			if ( user.can(`page:${this.UUID}:${level}`) ) user.disallow(`page:${this.UUID}:${level}`)
		}

		// Remove the user from this page's access lists
		this.shared_users_view = this.shared_users_view.filter(x => String(x) !== user.id)
		this.shared_users_update = this.shared_users_update.filter(x => String(x) !== user.id)
		this.shared_users_manage = this.shared_users_manage.filter(x => String(x) !== user.id)

		await this.version_save(`Unshared with ${user.uid}`, user.id)
		await user.save()
	}

	async get_menu_items() {
		// {
		// 	id: child.UUID,
		// 		name: child.is_shared() ? child.Name + ' ⁽ˢʰᵃʳᵉᵈ⁾' : child.Name,
		// 	shared: child.is_shared(),
		// 	children: await this._build_menu_object(child),
		// 	type: 'page',
		// }

		// Databases & Code Snips
		const children = []

		const Database = this.models.get('api:db:Database')
		const dbs = await Database.find({
			Active: true,
			PageId: this.UUID,
		})

		for ( const db of dbs ) {
			children.push({
				id: db.PageId,
				node_id: db.NodeId,
				children: [],
				type: 'db',
				name: db.Name,
			})
		}

		const Codium = this.models.get('api:Codium')
		const codiums = await Codium.find({
			PageId: this.UUID,
		})

		for ( const codium of codiums ) {
			children.push({
				id: codium.PageId,
				node_id: codium.NodeId,
				children: [],
				type: 'code',
				name: codium.code.slice(0, 25) + (codium.code.length > 25 ? '...' : ''),
			})
		}

		return children
	}

	async cast_to_version_data() {
		const data = await super.cast_to_version_data()
		const node_version_nums = []
		const nodes = await this.nodes

		for ( const node of nodes ) {
			node_version_nums.push({ NodeId: node.UUID, version_num: node.version_num })
		}

		data.node_version_nums = node_version_nums
		return data
	}
}

module.exports = exports = Page;
