import { Authorized, Body, Delete, Get, JsonController, OnUndefined, Param, Post, Put, QueryParam } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { Repository, getConnectionManager } from 'typeorm';
import { DonationIdsNotMatchingError, DonationNotFoundError } from '../errors/DonationErrors';
import { DonorNotFoundError } from '../errors/DonorErrors';
import { RunnerNotFoundError } from '../errors/RunnerErrors';
import { CreateAnonymousDonation } from '../models/actions/create/CreateAnonymousDonation';
import { CreateDistanceDonation } from '../models/actions/create/CreateDistanceDonation';
import { CreateFixedDonation } from '../models/actions/create/CreateFixedDonation';
import { UpdateDistanceDonation } from '../models/actions/update/UpdateDistanceDonation';
import { UpdateFixedDonation } from '../models/actions/update/UpdateFixedDonation';
import { DistanceDonation } from '../models/entities/DistanceDonation';
import { Donation } from '../models/entities/Donation';
import { FixedDonation } from '../models/entities/FixedDonation';
import { ResponseAnonymousDonation } from '../models/responses/ResponseAnonymousDonation';
import { ResponseDistanceDonation } from '../models/responses/ResponseDistanceDonation';
import { ResponseDonation } from '../models/responses/ResponseDonation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';

@JsonController('/donations')
@OpenAPI({ security: [{ "AuthToken": [] }, { "RefreshTokenCookie": [] }] })
export class DonationController {
	private donationRepository: Repository<Donation>;
	private distanceDonationRepository: Repository<DistanceDonation>;
	private fixedDonationRepository: Repository<FixedDonation>;

	/**
	 * Gets the repository of this controller's model/entity.
	 */
	constructor() {
		this.donationRepository = getConnectionManager().get().getRepository(Donation);
		this.distanceDonationRepository = getConnectionManager().get().getRepository(DistanceDonation);
		this.fixedDonationRepository = getConnectionManager().get().getRepository(FixedDonation);
	}

	@Get()
	@Authorized("DONATION:GET")
	@ResponseSchema(ResponseDonation, { isArray: true })
	@ResponseSchema(ResponseDistanceDonation, { isArray: true })
	@ResponseSchema(ResponseAnonymousDonation, { isArray: true })
	@OpenAPI({ description: 'Lists all donations (fixed or distance based) from all donors. <br> This includes the donations\'s runner\'s distance ran(if distance donation).' })
	async getAll(@QueryParam("page", { required: false }) page: number, @QueryParam("page_size", { required: false }) page_size: number = 100) {
		let responseDonations: ResponseDonation[] = new Array<ResponseDonation>();
		let donations: Array<Donation>;

		if (page != undefined) {
			donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'], skip: page * page_size, take: page_size });
		} else {
			donations = await this.donationRepository.find({ relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });
		}

		donations.forEach(donation => {
			responseDonations.push(donation.toResponse());
		});
		return responseDonations;
	}

	@Get('/:id')
	@Authorized("DONATION:GET")
	@ResponseSchema(ResponseDonation)
	@ResponseSchema(ResponseDistanceDonation)
	@ResponseSchema(ResponseAnonymousDonation)
	@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
	@OnUndefined(DonationNotFoundError)
	@OpenAPI({ description: 'Lists all information about the donation whose id got provided. This includes the donation\'s runner\'s distance ran (if distance donation).' })
	async getOne(@Param('id') id: number) {
		let donation = await this.donationRepository.findOne({ id: id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })
		if (!donation) { throw new DonationNotFoundError(); }
		return donation.toResponse();
	}

	@Post('/fixed')
	@Authorized("DONATION:CREATE")
	@ResponseSchema(ResponseDonation)
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
	@OpenAPI({ description: 'Create a fixed donation (not distance donation - use /donations/distance instead). <br> Please rmemember to provide the donation\'s donors\'s id and amount.' })
	async postFixed(@Body({ validate: true }) createDonation: CreateFixedDonation) {
		let donation = await createDonation.toEntity();
		donation = await this.fixedDonationRepository.save(donation);
		return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
	}

	@Post('/anonymous')
	@Authorized("DONATION:CREATE")
	@ResponseSchema(ResponseDonation)
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
	@OpenAPI({ description: 'Create a anonymous donation' })
	async postAnonymous(@Body({ validate: true }) createDonation: CreateAnonymousDonation) {
		let donation = await createDonation.toEntity();
		donation = await this.fixedDonationRepository.save(donation);
		return (await this.donationRepository.findOne({ id: donation.id })).toResponse();
	}

	@Post('/distance')
	@Authorized("DONATION:CREATE")
	@ResponseSchema(ResponseDistanceDonation)
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@OpenAPI({ description: 'Create a distance donation (not fixed donation - use /donations/fixed instead). <br> Please rmemember to provide the donation\'s donors\'s and runner\s ids and amount per distance (kilometer).' })
	async postDistance(@Body({ validate: true }) createDonation: CreateDistanceDonation) {
		let donation = await createDonation.toEntity();
		donation = await this.distanceDonationRepository.save(donation);
		return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
	}

	@Put('/fixed/:id')
	@Authorized("DONATION:UPDATE")
	@ResponseSchema(ResponseDonation)
	@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
	@OpenAPI({ description: "Update the fixed donation (not distance donation - use /donations/distance instead) whose id you provided. <br> Please remember that ids can't be changed and amounts must be positive." })
	async putFixed(@Param('id') id: number, @Body({ validate: true }) donation: UpdateFixedDonation) {
		let oldDonation = await this.fixedDonationRepository.findOne({ id: id });

		if (!oldDonation) {
			throw new DonationNotFoundError();
		}

		if (oldDonation.id != donation.id) {
			throw new DonationIdsNotMatchingError();
		}

		await this.fixedDonationRepository.save(await donation.update(oldDonation));
		return (await this.donationRepository.findOne({ id: donation.id }, { relations: ['donor'] })).toResponse();
	}

	@Put('/distance/:id')
	@Authorized("DONATION:UPDATE")
	@ResponseSchema(ResponseDonation)
	@ResponseSchema(DonationNotFoundError, { statusCode: 404 })
	@ResponseSchema(DonorNotFoundError, { statusCode: 404 })
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@ResponseSchema(DonationIdsNotMatchingError, { statusCode: 406 })
	@OpenAPI({ description: "Update the distance donation (not fixed donation - use /donations/fixed instead) whose id you provided. <br> Please remember that ids can't be changed and amountPerDistance must be positive." })
	async putDistance(@Param('id') id: number, @Body({ validate: true }) donation: UpdateDistanceDonation) {
		let oldDonation = await this.distanceDonationRepository.findOne({ id: id });

		if (!oldDonation) {
			throw new DonationNotFoundError();
		}

		if (oldDonation.id != donation.id) {
			throw new DonationIdsNotMatchingError();
		}

		await this.distanceDonationRepository.save(await donation.update(oldDonation));
		return (await this.distanceDonationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] })).toResponse();
	}

	@Delete('/:id')
	@Authorized("DONATION:DELETE")
	@ResponseSchema(ResponseDonation)
	@ResponseSchema(ResponseDistanceDonation)
	@ResponseSchema(ResponseEmpty, { statusCode: 204 })
	@OnUndefined(204)
	@OpenAPI({ description: 'Delete the donation whose id you provided. <br> If no donation with this id exists it will just return 204(no content).' })
	async remove(@Param("id") id: number, @QueryParam("force") force: boolean) {
		let donation = await this.donationRepository.findOne({ id: id });
		if (!donation) { return null; }
		const responseScan = await this.donationRepository.findOne({ id: donation.id }, { relations: ['runner', 'donor', 'runner.scans', 'runner.scans.track'] });

		await this.donationRepository.delete(donation);
		return responseScan.toResponse();
	}
}
