/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ro.nextreports.server.web;

import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.http.protocol.HTTP;
import org.apache.wicket.Page;
import org.apache.wicket.RestartResponseAtInterceptPageException;
import org.apache.wicket.Session;
import org.apache.wicket.authorization.IAuthorizationStrategy;
import org.apache.wicket.authorization.strategies.page.SimplePageAuthorizationStrategy;
import org.apache.wicket.core.request.handler.PageProvider;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
import org.apache.wicket.core.request.mapper.StalePageException;
import org.apache.wicket.devutils.DevUtilsPage;
import org.apache.wicket.markup.html.pages.RedirectPage;
import org.apache.wicket.protocol.http.PageExpiredException;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.protocol.http.servlet.ServletWebRequest;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Response;
import org.apache.wicket.request.cycle.AbstractRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.spring.injection.annot.SpringComponentInjector;
import org.apache.wicket.util.encoding.UrlEncoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.context.support.WebApplicationContextUtils;

import ro.nextreports.server.ReleaseInfo;
import ro.nextreports.server.StorageConstants;
import ro.nextreports.server.dao.StorageDao;
import ro.nextreports.server.domain.Entity;
import ro.nextreports.server.domain.IFrameSettings;
import ro.nextreports.server.domain.SchedulerJob;
import ro.nextreports.server.domain.Settings;
import ro.nextreports.server.domain.User;
import ro.nextreports.server.exception.MaintenanceException;
import ro.nextreports.server.schedule.QuartzJobHandler;
import ro.nextreports.server.schedule.UserSynchronizerJob;
import ro.nextreports.server.service.StorageService;
import ro.nextreports.server.web.common.misc.NoVersionMountMapper;
import ro.nextreports.server.web.core.ErrorPage;
import ro.nextreports.server.web.core.HomePage;
import ro.nextreports.server.web.core.MaintenancePage;
import ro.nextreports.server.web.core.SecurePage;
import ro.nextreports.server.web.core.settings.LogoResourceReference;
import ro.nextreports.server.web.dashboard.WidgetWebPage;
import ro.nextreports.server.web.debug.Info;
import ro.nextreports.server.web.debug.InfoUtil;
import ro.nextreports.server.web.debug.SystemInfoPage;
import ro.nextreports.server.web.debug.SystemLogPage;
import ro.nextreports.server.web.integration.DashboardWebPage;
import ro.nextreports.server.web.integration.DashboardsPage;
import ro.nextreports.server.web.integration.ReportsPage;
import ro.nextreports.server.web.security.LoginPage;
import ro.nextreports.server.web.security.SecurityUtil;
import ro.nextreports.server.web.security.cas.CasLoginErrorPage;
import ro.nextreports.server.web.security.cas.CasLoginPage;
import ro.nextreports.server.web.security.cas.CasUtil;
import ro.nextreports.server.web.security.recover.ForgotPasswordPage;
import ro.nextreports.server.web.security.recover.ResetPasswordPage;
import ro.nextreports.server.web.themes.ThemesManager;

/**
 * @author Decebal Suiu
 */
public class NextServerApplication extends WebApplication {

	public final static String NEXT_CHARTS_JS = "nextcharts-1.5.min.js";

	private static volatile boolean maintenance = false;
	private static final Logger LOG = LoggerFactory.getLogger(NextServerApplication.class);

	public NextServerApplication() {
		super();
	}

	public static NextServerApplication get() {
		return (NextServerApplication) WebApplication.get();
	}

	@Override
	public void init() {
		super.init();

		// log system info
		logSystemInfo();

		// spring
		addSpringInjection();

		// markup settings
		getMarkupSettings().setStripWicketTags(true);
		getMarkupSettings().setDefaultMarkupEncoding("UTF-8");

		// application settings
		if (CasUtil.isCasUsed()) {
			getApplicationSettings().setPageExpiredErrorPage(CasLoginPage.class);
			// getApplicationSettings().setInternalErrorPage(CasLoginErrorPage.class);
			getApplicationSettings().setAccessDeniedPage(CasLoginPage.class);
		} else {
			getApplicationSettings().setPageExpiredErrorPage(LoginPage.class);
			// getApplicationSettings().setInternalErrorPage(LoginErrorPage.class);
			getApplicationSettings().setAccessDeniedPage(LoginPage.class);
		}

		// show internal error page rather than default developer page
		// getExceptionSettings().setUnexpectedExceptionDisplay(IExceptionSettings.SHOW_INTERNAL_ERROR_PAGE);

		// exception settings
		getResourceSettings().setThrowExceptionOnMissingResource(false);

		// security settings
		addSecurityAuthorization();

		// request cycle settings
		// getRequestCycleSettings().addResponseFilter(new
		// ServerAndClientTimeFilter());
		// remove this so meta content is first in head title
		// getRequestCycleSettings().addResponseFilter(new
		// AjaxServerAndClientTimeFilter());

		// debug
		// getDebugSettings().setAjaxDebugModeEnabled(false);
		getDebugSettings().setDevelopmentUtilitiesEnabled(true);

		// activate some options only in "DEVELOPMENT" mode
		if (usesDevelopmentConfig()) {
			// enable request logger
			getRequestLoggerSettings().setRequestLoggerEnabled(true);
			getRequestLoggerSettings().setRequestsWindowSize(3000);

			// locate where wicket markup comes from your browser's source view
			getDebugSettings().setOutputMarkupContainerClassName(true);
		}

		// mount
		// new
		// AnnotatedMountScanner().scanPackage(NextServerApplication.class.getPackage().getName()).mount(this);

		mount(new NoVersionMountMapper("/home", HomePage.class));

		if (CasUtil.isCasUsed()) {
			// mountPage("/login", CasLoginPage.class);
			mount(new NoVersionMountMapper("/login", CasLoginPage.class));
			// this matches the value set in securityCas.xml
			// mountPage("/cas/error", CasLoginErrorPage.class);
			mount(new NoVersionMountMapper("/cas/error", CasLoginErrorPage.class));
		} else {
			// mountPage("/login", LoginPage.class);
			mount(new NoVersionMountMapper("/login", LoginPage.class));
		}
		mountPage("/debug", DevUtilsPage.class);
		mountPage("/sysinfo", SystemInfoPage.class);
		mountPage("/syslog", SystemLogPage.class);
		// mountPage("/addFolders", AddFoldersPage.class); // for development
		// mountPage("/pivot", PivotPage.class); // for development
		mountPage("/forgot", ForgotPasswordPage.class);
		mountPage("/reset", ResetPasswordPage.class);
		mountPage("/dashboards", DashboardsPage.class);
		mountPage("/reports", ReportsPage.class);

		// load all jobs from repository to scheduler
		addJobsInScheduler();

		StorageService storageService = (StorageService) getSpringBean("storageService");
		if (storageService.getSettings() != null) {
			if (storageService.getSettings().getSynchronizer().isRunOnStartup()) {
				runUserSynchronizerJob();
			}

			IFrameSettings iframeSettings = storageService.getSettings().getIframe();
			if ((iframeSettings != null) && iframeSettings.isEnable()) {
				mount(new NoVersionMountMapper("/widget", WidgetWebPage.class));
				mount(new NoVersionMountMapper("/dashboard", DashboardWebPage.class));
			}

			// set the current color theme at startup
			ThemesManager.getInstance().setTheme(storageService.getSettings().getColorTheme());

			// need to have a static url to view logo in maintenance page
			mountResource("/../themes/" + storageService.getSettings().getColorTheme() + "/images/Nextreports-logo.png",
					new LogoResourceReference());
		}

		getRequestCycleListeners().add(new ExceptionRequestCycleListener());
		getRequestCycleListeners().add(new LoggingRequestCycleListener());
		getRequestCycleListeners().add(new MaintenanceRequestCycleListener());

		logSettings(storageService.getSettings());

		LOG.info("NextReports Server " + ReleaseInfo.getVersion() + " started.");
	}

	@Override
	public Class<? extends Page> getHomePage() {
		return HomePage.class;
	}

	@Override
	public Session newSession(Request request, Response response) {
		return new NextServerSession(request);
	}

	public Object getSpringBean(String beanName) {
		ApplicationContext applicationContext = WebApplicationContextUtils
				.getWebApplicationContext(getServletContext());
		if (!applicationContext.containsBean(beanName)) {
			return null;
		}

		return applicationContext.getBean(beanName);
	}

	protected void addSpringInjection() {
		getComponentInstantiationListeners().add(new SpringComponentInjector(this));
	}

	protected void addSecurityAuthorization() {
		Class<? extends Page> signInPageClass = LoginPage.class;
		if (CasUtil.isCasUsed()) {
			signInPageClass = CasLoginPage.class;
		}

		IAuthorizationStrategy authStrategy = new SimplePageAuthorizationStrategy(SecurePage.class, signInPageClass) {

			@Override
			protected boolean isAuthorized() {
				boolean b = NextServerSession.get().isSignedIn();
				if (!b) {
					if (CasUtil.isCasUsed()) {
						LOG.debug("Checking if context contains CAS authentication");
						b = NextServerSession.get().checkForSignIn();
						if (!b) {
							String serviceUrl = CasUtil.getServiceProperties().getService();
							String loginUrl = CasUtil.getLoginUrl();
							LOG.debug("cas authentication: service URL: " + serviceUrl);
							String redirectUrl = loginUrl + "?service=" + serviceUrl;
							LOG.debug("attempting to redirect to: " + redirectUrl);
							throw new RestartResponseAtInterceptPageException(new RedirectPage(redirectUrl));
						}
					}
				}

				return b;
			}

		};
		getSecuritySettings().setAuthorizationStrategy(authStrategy);
	}

	protected void addJobsInScheduler() {
		if (LOG.isDebugEnabled()) {
			LOG.debug("Add jobs in scheduler...");
		}
		long t = System.currentTimeMillis();

		SchedulerJob[] schedulerJobs = getSchedulerJobs();
		Object o = getSpringBean("quartzJobHandler");
		if (o != null) {
			QuartzJobHandler quartzJobHandler = (QuartzJobHandler) o;
			if (schedulerJobs != null) {
				for (SchedulerJob schedulerJob : schedulerJobs) {
					try {
						quartzJobHandler.addJob(schedulerJob);
					} catch (Exception e) {
						// TODO
						e.printStackTrace();
						LOG.error(e.getMessage(), e);
					}
				}
			}
		}

		if (LOG.isDebugEnabled()) {
			t = System.currentTimeMillis() - t;
			LOG.debug("Added jobs in scheduler in " + t + " ms");
		}
	}

	private SchedulerJob[] getSchedulerJobs() {
		PlatformTransactionManager transactionManager = (PlatformTransactionManager) getSpringBean(
				"transactionManager");
		TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
		SchedulerJob[] schedulerJobs = transactionTemplate.execute(new TransactionCallback<SchedulerJob[]>() {

			public SchedulerJob[] doInTransaction(TransactionStatus transactionStatus) {
				StorageDao storageDao = (StorageDao) getSpringBean("storageDao");
				try {
					Entity[] entities = storageDao.getEntitiesByClassName(StorageConstants.SCHEDULER_ROOT,
							SchedulerJob.class.getName());
					SchedulerJob[] schedulerJobs = new SchedulerJob[entities.length];
					System.arraycopy(entities, 0, schedulerJobs, 0, entities.length);

					return schedulerJobs;
				} catch (Exception e) {
					// TODO
					e.printStackTrace();
					transactionStatus.setRollbackOnly();
					return null;
				}
			}

		});

		return schedulerJobs;
	}

	private void runUserSynchronizerJob() {
		if (LOG.isDebugEnabled()) {
			LOG.debug("Run user synchronizer job ...");
		}
		long t = System.currentTimeMillis();

		// JobDetail userSynchronizerJob = (JobDetail)
		// getSpringBean("userSynchronizerJob");
		ProviderManager authenticationManager = (ProviderManager) getSpringBean("authenticationManager");
		UserSynchronizerJob userSynchronizerJob = new UserSynchronizerJob();
		userSynchronizerJob.setAuthenticationManager(authenticationManager);
		userSynchronizerJob.setStorageService((StorageService) getSpringBean("storageService"));
		userSynchronizerJob.syncUsers();

		if (LOG.isDebugEnabled()) {
			t = System.currentTimeMillis() - t;
			LOG.debug("Users synchronized in " + t + " ms");
		}
	}

	public static boolean isMaintenance() {
		return maintenance;
	}

	public static void setMaintenance(boolean maintenance) {
		NextServerApplication.maintenance = maintenance;
	}

	private void logSystemInfo() {
		LOG.info("############ S Y S T E M    P R O P E R T I E S ############");
		List<String> names = InfoUtil.getSystemProperties();
		for (String name : names) {
			LOG.info(String.format("%-40s", name) + " : " + System.getProperty(name));
		}

		LOG.info("############ J V M    A R G U M E N T S ############");
		List<String> arguments = InfoUtil.getJVMArguments();
		for (String argument : arguments) {
			LOG.info(argument);
		}

		LOG.info("############ G E N E R A L    J V M     I N F O ############");
		List<Info> infos = InfoUtil.getGeneralJVMInfo();
		for (Info info : infos) {
			LOG.info(String.format("%-20s", info.getDisplayName()) + " : " + info.getValue());
		}

		LOG.info("############ E N D     S Y S T E M     I N F O ############");
	}

	private void logSettings(Settings settings) {
		LOG.info("############ S E R V E R    S E T T I N G S ############");
		List<Info> infos = InfoUtil.getServerSettings(settings);
		for (Info info : infos) {
			LOG.info(String.format("%-40s", info.getDisplayName()) + " : " + info.getValue());
		}
		LOG.info("############ E N D    S E R V E R    S E T T I N G S ############");
	}

	private class ExceptionRequestCycleListener extends AbstractRequestCycleListener {

		@Override
		public IRequestHandler onException(RequestCycle cycle, Exception e) {
			if (e instanceof PageExpiredException) {
				LOG.error("Page expired", e); // !?
				return null; // see
								// getApplicationSettings().setPageExpiredErrorPage
			}

			if (e instanceof MaintenanceException) {
				return new RenderPageRequestHandler(new PageProvider(MaintenancePage.class));
			}

			if (e instanceof StalePageException) {
				return null;
			}

			String errorCode = String.valueOf(System.currentTimeMillis());
			LOG.error("Error with code " + errorCode, e);

			PageParameters parameters = new PageParameters();
			parameters.add("errorCode", errorCode);
			parameters.add("errorMessage", UrlEncoder.QUERY_INSTANCE.encode(e.getMessage(), HTTP.ISO_8859_1));
			return new RenderPageRequestHandler(new PageProvider(ErrorPage.class, parameters));
		}

	}

	private class LoggingRequestCycleListener extends AbstractRequestCycleListener {

		@Override
		public void onBeginRequest(RequestCycle cycle) {
			String username = "";
			if (NextServerSession.get().isSignedIn()) {
				username = NextServerSession.get().getUsername();
			}

			Session session = NextServerSession.get();
			String sessionId = NextServerSession.get().getId();
			if (sessionId == null) {
				session.bind();
				sessionId = session.getId();
			}

			HttpServletRequest request = ((ServletWebRequest) RequestCycle.get().getRequest()).getContainerRequest();
			String ip = request.getHeader("X-Forwarded-For");
			if (ip == null) {
				ip = request.getRemoteHost();
			}

			MDC.put("username", username);
			MDC.put("session", sessionId);
			MDC.put("ip", ip);
		}

		@Override
		public void onEndRequest(RequestCycle cycle) {
			MDC.remove("username");
			MDC.remove("session");
			MDC.remove("ip");
		}

	}

	private class MaintenanceRequestCycleListener extends AbstractRequestCycleListener {

		@Override
		public void onBeginRequest(RequestCycle cycle) {
			if (isMaintenance()) {
				User user = SecurityUtil.getLoggedUser();
				if ((user == null) || ((user != null) && user.isAdmin())) {
					super.onBeginRequest(cycle);
					return;
				}

				throw new MaintenanceException();
			}
		}

	}

}