import type { Request } from "express";
import * as jwt from "jsonwebtoken";
import { BadRequestError, Body, Delete, Get, JsonController, OnUndefined, Param, Post, QueryParam, Req, UseBefore } from 'routing-controllers';
import { OpenAPI, ResponseSchema } from 'routing-controllers-openapi';
import { getConnectionManager, Repository } from 'typeorm';
import { config } from '../config';
import { InvalidCredentialsError, JwtNotProvidedError } from '../errors/AuthError';
import { MailSendingError } from '../errors/MailErrors';
import { RunnerEmailNeededError, RunnerHasDistanceDonationsError, RunnerNotFoundError, RunnerSelfserviceTimeoutError } from '../errors/RunnerErrors';
import { RunnerOrganizationNotFoundError } from '../errors/RunnerOrganizationErrors';
import { ScanStationNotFoundError } from '../errors/ScanStationErrors';
import { JwtCreator } from '../jwtcreator';
import { Mailer } from '../mailer';
import ScanAuth from '../middlewares/ScanAuth';
import { CreateSelfServiceCitizenRunner } from '../models/actions/create/CreateSelfServiceCitizenRunner';
import { CreateSelfServiceRunner } from '../models/actions/create/CreateSelfServiceRunner';
import { Runner } from '../models/entities/Runner';
import { RunnerGroup } from '../models/entities/RunnerGroup';
import { RunnerOrganization } from '../models/entities/RunnerOrganization';
import { ScanStation } from '../models/entities/ScanStation';
import { ResponseEmpty } from '../models/responses/ResponseEmpty';
import { ResponseScanStation } from '../models/responses/ResponseScanStation';
import { ResponseSelfServiceOrganisation } from '../models/responses/ResponseSelfServiceOrganisation';
import { ResponseSelfServiceRunner } from '../models/responses/ResponseSelfServiceRunner';
import { ResponseSelfServiceScan } from '../models/responses/ResponseSelfServiceScan';
import { DonationController } from './DonationController';
import { RunnerCardController } from './RunnerCardController';
import { ScanController } from './ScanController';

@JsonController()
export class RunnerSelfServiceController {
	private runnerRepository: Repository<Runner>;
	private orgRepository: Repository<RunnerOrganization>;
	private stationRepository: Repository<ScanStation>;

	/**
	 * Gets the repository of this controller's model/entity.
	 */
	constructor() {
		this.runnerRepository = getConnectionManager().get().getRepository(Runner);
		this.orgRepository = getConnectionManager().get().getRepository(RunnerOrganization);
		this.stationRepository = getConnectionManager().get().getRepository(ScanStation);
	}

	@Get('/runners/me/:jwt')
	@ResponseSchema(ResponseSelfServiceRunner)
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@OnUndefined(RunnerNotFoundError)
	@OpenAPI({ description: 'Lists all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
	async get(@Param('jwt') token: string) {
		return (new ResponseSelfServiceRunner(await this.getRunner(token)));
	}

	@Delete('/runners/me/:jwt')
	@ResponseSchema(ResponseSelfServiceRunner)
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@OnUndefined(RunnerNotFoundError)
	@OpenAPI({ description: 'Deletes all information about yourself. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please use the forgot endpoint.' })
	async remove(@Param('jwt') token: string, @QueryParam("force") force: boolean) {
		const responseRunner = await this.getRunner(token);
		let runner = await this.runnerRepository.findOne({ id: responseRunner.id });

		if (!runner) { return null; }
		if (!runner) {
			throw new RunnerNotFoundError();
		}

		const runnerDonations = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["distanceDonations"] })).distanceDonations;
		if (runnerDonations.length > 0 && !force) {
			throw new RunnerHasDistanceDonationsError();
		}
		const donationController = new DonationController();
		for (let donation of runnerDonations) {
			await donationController.remove(donation.id, force);
		}

		const runnerCards = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["cards"] })).cards;
		const cardController = new RunnerCardController;
		for (let card of runnerCards) {
			await cardController.remove(card.id, force);
		}

		const runnerScans = (await this.runnerRepository.findOne({ id: runner.id }, { relations: ["scans"] })).scans;
		const scanController = new ScanController;
		for (let scan of runnerScans) {
			await scanController.remove(scan.id, force);
		}

		await this.runnerRepository.delete(runner);
		return new ResponseSelfServiceRunner(responseRunner);
	}

	@Get('/runners/me/:jwt/scans')
	@ResponseSchema(ResponseSelfServiceScan, { isArray: true })
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@OnUndefined(RunnerNotFoundError)
	@OpenAPI({ description: 'Lists all your (runner) scans. <br> Please provide your runner jwt(that code we gave you during registration) for auth. <br> If you lost your jwt/personalized link please contact support.' })
	async getScans(@Param('jwt') token: string) {
		const scans = (await this.getRunner(token)).scans;
		let responseScans = new Array<ResponseSelfServiceScan>()
		for (let scan of scans) {
			responseScans.push(new ResponseSelfServiceScan(scan));
		}
		return responseScans;
	}

	@Get('/stations/me')
	@UseBefore(ScanAuth)
	@ResponseSchema(ResponseScanStation)
	@ResponseSchema(ScanStationNotFoundError, { statusCode: 404 })
	@OnUndefined(ScanStationNotFoundError)
	@OpenAPI({ description: 'Lists basic information about the station whose token got provided. <br> This includes it\'s associated track.', security: [{ "StationApiToken": [] }] })
	async getStationMe(@Req() req: Request) {
		let scan = await this.stationRepository.findOne({ id: parseInt(req.headers["station_id"].toString()) }, { relations: ['track'] })
		if (!scan) { throw new ScanStationNotFoundError(); }
		return scan.toResponse();
	}

	@Post('/runners/login')
	@ResponseSchema(RunnerNotFoundError, { statusCode: 404 })
	@OnUndefined(ResponseEmpty)
	@OpenAPI({ description: 'Use this endpoint to reuqest a new selfservice magic-login-link to be sent to your mail address (rate limited to one mail every 15mins).' })
	async requestNewToken(@QueryParam('mail') mail: string, @QueryParam("locale") locale: string = "en") {
		if (!mail) {
			throw new RunnerNotFoundError();
		}
		const runner = await this.runnerRepository.findOne({ email: mail });
		if (!runner) { throw new RunnerNotFoundError(); }

		if (runner.resetRequestedTimestamp > (Math.floor(Date.now() / 1000) - 30)) { throw new RunnerSelfserviceTimeoutError(); }
		const token = JwtCreator.createSelfService(runner);

		try {
			await Mailer.sendSelfserviceForgottenMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, token, locale)
		} catch (error) {
			throw new MailSendingError();
		}

		runner.resetRequestedTimestamp = Math.floor(Date.now() / 1000);
		await this.runnerRepository.save(runner);

		return { token };
	}

	@Post('/runners/register')
	@ResponseSchema(ResponseSelfServiceRunner)
	@ResponseSchema(RunnerEmailNeededError, { statusCode: 406 })
	@OpenAPI({ description: 'Create a new selfservice runner in the citizen org. <br> This endpoint shoud be used to allow "everyday citizen" to register themselves. <br> You have to provide a mail address, b/c the future we\'ll implement email verification.' })
	async registerRunner(@Body({ validate: true }) createRunner: CreateSelfServiceCitizenRunner, @QueryParam("locale") locale: string = "en") {
		let runner = await createRunner.toEntity();
		if (await this.getRunnerExistsByMail(runner.email)) {
			throw new BadRequestError("E-Mail already registered")
		}
		runner = await this.runnerRepository.save(runner);

		let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
		response.token = JwtCreator.createSelfService(runner);

		try {
			await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
		} catch (error) {
			throw new MailSendingError();
		}

		return response;
	}

	@Post('/runners/register/:token')
	@ResponseSchema(ResponseSelfServiceRunner)
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
	@OpenAPI({ description: 'Create a new selfservice runner in a provided org. <br> The orgs get provided and authorized via api tokens that can be optained via the /organizations endpoint.' })
	async registerOrganizationRunner(@Param('token') token: string, @Body({ validate: true }) createRunner: CreateSelfServiceRunner, @QueryParam("locale") locale: string = "en") {
		const org = await this.getOrgansisation(token);

		let runner = await createRunner.toEntity(org);
		if (await this.getRunnerExistsByMail(runner.email)) {
			throw new BadRequestError("E-Mail already registered")
		}
		runner = await this.runnerRepository.save(runner);

		let response = new ResponseSelfServiceRunner(await this.runnerRepository.findOne(runner, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] }));
		response.token = JwtCreator.createSelfService(runner);

		try {
			await Mailer.sendSelfserviceWelcomeMail(runner.email, runner.id, runner.firstname, runner.middlename, runner.lastname, response.token, locale)
		} catch (error) {
			throw new MailSendingError();
		}

		return response;
	}

	@Get('/organizations/selfservice/:token')
	@ResponseSchema(ResponseSelfServiceOrganisation, { isArray: false })
	@ResponseSchema(RunnerOrganizationNotFoundError, { statusCode: 404 })
	@OpenAPI({ description: 'Get the basic info and teams for a org.' })
	async getSelfserviceOrg(@Param('token') token: string) {
		const orgid = (await this.getOrgansisation(token)).id;
		const org = await this.orgRepository.findOne({ id: orgid }, { relations: ['teams'] })

		return new ResponseSelfServiceOrganisation(<RunnerOrganization>org);
	}

	/**
	 * Get's a runner by a provided jwt token.
	 * @param token The runner jwt provided by the runner to identitfy themselves.
	 */
	private async getRunner(token: string): Promise<Runner> {
		if (token == "") { throw new JwtNotProvidedError(); }
		let jwtPayload = undefined
		try {
			jwtPayload = <any>jwt.verify(token, config.jwt_secret);
		} catch (error) {
			throw new InvalidCredentialsError();
		}

		const runner = await this.runnerRepository.findOne({ id: jwtPayload["id"] }, { relations: ['scans', 'group', 'group.parentGroup', 'scans.track', 'cards', 'distanceDonations', 'distanceDonations.donor', 'distanceDonations.runner', 'distanceDonations.runner.scans', 'distanceDonations.runner.scans.track'] });
		if (!runner) { throw new RunnerNotFoundError() }
		return runner;
	}

	/**
	 * Get's a runner org by a provided registration api key.
	 * @param token The organization's registration api token.
	 */
	private async getOrgansisation(token: string): Promise<RunnerGroup> {
		token = Buffer.from(token, 'base64').toString('utf8');

		const organization = await this.orgRepository.findOne({ key: token });
		if (!organization) { throw new RunnerOrganizationNotFoundError; }

		return organization;
	}

	/**
	 * Checks if a runner already exists
	 * @param email The runner's email address
	 * @returns Boolean (true if exists, false if not)
	 */
	private async getRunnerExistsByMail(email: string): Promise<boolean> {
		const runner = await this.runnerRepository.findOne({ email });
		return runner != undefined
	}
}