import { Request, Response } from 'express'
import { Model, ModelClass } from 'objection'
import { normalize, schema as nSchema } from 'normalizr'
import DBService, { Builder, BuilderSingle } from '../services/db/DBService'

export type RequestHandler = (
	request: Request,
	response: Response
) => Promise<Response>
export type Handler<R extends Request> = (
	request: R,
	response: Response
) => Promise<Response>

export default class BaseController<T extends Model, U extends DBService<T>> {
	protected service: U
	schema: nSchema.Entity
	arraySchema: nSchema.Array

	constructor(modelClass: ModelClass<T>, normalizrSchema: nSchema.Entity) {
		this.service = new DBService<T>(modelClass) as U
		this.schema = normalizrSchema
		this.arraySchema = new nSchema.Array(normalizrSchema)
	}

	public getAll: RequestHandler = async (request, response) => {
		interface Query {
			userId?: string
		}
		const query: Query = request.query

		const dbQuery = (builder: Builder<T>) =>
			query.userId ? builder.where('userId', query.userId) : builder

		try {
			let data: T[] = await this.service.findAll(dbQuery)
			const normalizedData = normalize(
				data.map(v => v.toJSON()),
				this.arraySchema
			)
			return response.send(normalizedData)
		} catch (err) {
			console.log(err)
			return response.status(404).send(err)
		}
	}

	public get: RequestHandler = async (request, response) => {
		interface Query {
			userId?: string
		}
		interface Params {
			id: string
		}
		const query: Query = request.query
		const params: Params = request.params as any
		const id = parseInt(params.id, 10)

		const dbQuery = (builder: BuilderSingle<T>) =>
			query.userId ? builder.where('userId', query.userId) : builder

		try {
			let data: T = await this.service.findById(id, dbQuery)
			const normalizedData = normalize(data.toJSON(), this.schema)
			return response.send(normalizedData)
		} catch (err) {
			console.log(err)
			return response.status(404).send(err)
		}
	}

	public create: RequestHandler = async (request, response) => {
		const body: any = request.body
		try {
			let data: T = await this.service.create(body)
			const normalizedData = normalize(data.toJSON(), this.schema)
			return response.send(normalizedData)
		} catch (err) {
			console.log(err)
			return response.status(404).send(err)
		}
	}

	public edit: RequestHandler = async (request, response) => {
		const id: number = request.params.id as any
		const body: any = request.body

		try {
			let data: T = await this.service.patch(
				{ id, ...body },
				{ relate: true, allowInsert: true }
			)
			const normalizedData = normalize(data.toJSON(), this.schema)
			return response.send(normalizedData)
		} catch (err) {
			console.log(err)
			return response.status(404).send(err)
		}
	}

	public delete: RequestHandler = async (request, response) => {
		const id: number = request.params.id as any

		try {
			await this.service.deleteById(id)
			return response.sendStatus(204)
		} catch (err) {
			console.log(err)
			return response.status(404).send(err)
		}
	}

	protected normalize = (
		data: T | T[],
		schema: nSchema.Entity = this.schema
	) => {
		if (Array.isArray(data)) {
			return normalize(
				data.map(i => i.toJSON() as T),
				new nSchema.Array(schema)
			)
		} else {
			return normalize(data.toJSON(), schema)
		}
	}
}
