package es.uv.saic.web;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import es.uv.saic.domain.Instancia;
import es.uv.saic.domain.InstanciaTasca;
import es.uv.saic.domain.InstanciaTascaTransfer;
import es.uv.saic.domain.InstanciaTascaVer;
import es.uv.saic.domain.InstanciaTransfer;
import es.uv.saic.domain.Organ;
import es.uv.saic.domain.Plantilla;
import es.uv.saic.domain.Proces;
import es.uv.saic.domain.Usuari;
import es.uv.saic.dto.InstanciaTascaDTO;
import es.uv.saic.dto.PdfDTO;
import es.uv.saic.dto.TemplateDataDTO;
import es.uv.saic.feign.PlantillaClient;
import es.uv.saic.service.*;


@Controller
public class ProceduresController {

    private final ProcesService procesService;
	 
	@Autowired
	private UsuariService us;
	@Autowired
	private InstanciaService is;
	@Autowired
	private InstanciaTascaService its;
	@Autowired
	private InstanciaTascaVerService itsver;
	@Autowired
	private OrganService ors;
	@Autowired
	private PlantillaClient plc;
	@Value("${saic.data.filePath}")
	private String filePath;

    ProceduresController(ProcesService procesService) {
        this.procesService = procesService;
    }
		
	/* Redirect root to /procedures */
	@GetMapping("/")
	public void getRoot(Model model, Authentication auth, HttpSession session, HttpServletResponse response) throws IOException {
		response.sendRedirect("/procedures");
	}
	
	/*
	 * Load the list of active procedure instances for the logged-in user
	 * 
	 * @param model 
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param _new Optional parameter to indicate a new access
	 * @return The name of the view to render
	 */
	@GetMapping("/procedures")
	public String getActiveInstances(Model model, Authentication auth, HttpSession session, @RequestParam(required = false) String _new) {
		
		if(_new != null) {
			model.addAttribute("new_access", true);
		}
		session.setAttribute("location", "procedures");
		
		Usuari u = (Usuari)auth.getPrincipal();
		List<InstanciaTransfer> plist = new ArrayList<InstanciaTransfer>();
		Instancia i;
		InstanciaTransfer it;
		InstanciaTasca itt;
		for(BigInteger p_id: this.us.getActiveInstancies(u)) {
			i = this.is.findByID(p_id);
			it = new InstanciaTransfer();
			it.setIdInstancia(i.getIdInstancia());
			it.setIdProces(i.getProces().getIdProces());
			it.setNomProces(i.getProces().getNomProces());
			it.setCursAvaluat(i.getProces().getCursAvaluat());
			it.setCursActivacio(i.getProces().getCursActivacio());
			it.setTitolCas(i.getProces().getTitolCas());
			it.setTitolVal(i.getProces().getTitolVal());
			it.setDescripcioCas(i.getProces().getDescripcioCas());
			it.setDescripcioVal(i.getProces().getDescripcioVal());
			it.setEstat(i.getEstat());
			if(i.getOrgan().getId().getTlugar().equals("C")) {
				Organ o = ors.findByID("T", i.getTitulacio());
				it.setTitulacioCas(o.getNomCas());
				it.setTitulacioVal(o.getNomVal());
				it.setCentreCas(i.getOrgan().getNomCas());
				it.setCentreVal(i.getOrgan().getNomVal());
			}
			else {
				it.setRuct(i.getOrgan().getRuct());
				it.setTitulacioCas(i.getOrgan().getNomCas());
				it.setTitulacioVal(i.getOrgan().getNomVal());
				it.setCentreCas(i.getOrgan().getOrgan().getNomCas());
				it.setCentreVal(i.getOrgan().getOrgan().getNomVal());
			}
			itt = its.findActiveByInstancia(i.getIdInstancia());
			it.setDataLimTascaActiva(itt.getTasca().getDataLim());
			it.setDataTascaActiva(itt.getData());
			it.setNomTascaActivaCas(itt.getTasca().getTitolCas());
			it.setNomTascaActivaVal(itt.getTasca().getTitolVal());
			it.setDescTascaActivaCas(itt.getTasca().getDescripcioCas());
			it.setDescTascaActivaVal(itt.getTasca().getDescripcioVal());
			it.setIdTascapActiva(itt.getTasca().getIdTascap());
			it.setTascaActivaExpired(itt.getTasca().isExpired());
			it.setTascaActivaAssignedToUser(its.isUserAuthorized(((Usuari)auth.getPrincipal()), itt.getIdInstanciaTasca()));
			plist.add(it);
		}
		model.addAttribute("procedure_list", plist);
		return "procedures";
	}
	
	/*
	 * Loads a procedure instance and its tasks
	 * 
	 * @param model 
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id Instancia ID Instance to load
	 * @return The name of the view to render
	 */
	@GetMapping("/procedure/{id}")
	public String getInstance(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id) {
		if(session.getAttribute("location") != null) {
			model.addAttribute("location", session.getAttribute("location"));
		}
		else {
			session.setAttribute("location", "procedures");
			model.addAttribute("location", "procedures");
		}
		this.loadProcedure(model, auth, id);
		return "procedure";
	}
	

	/*
	 * Updates a task instance with evidence files (for specific task types)
	 * 
	 * @param model
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id Instancia ID Instance to load
	 * @param params Form parameters
	 * @param evidencias List of evidence files (if any)
	 * @return The number of files uploaded (as a String)
	 * @throws IllegalStateException
	 * @throws IOException
	 * @throws InterruptedException
	 */
	@PostMapping("/procedure/files/{id}")
	@ResponseBody 
	public String updateInstanciaTascaEvidencia(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id, @RequestParam Map<String,String> params, 
			@RequestParam(required = true) List<MultipartFile> evidencias) throws IllegalStateException, IOException {
		session.setAttribute("location", "procedures");
		model.addAttribute("location", "procedures");
		InstanciaTasca it = its.findById(new BigInteger(params.get("taskid")));
		
		/* Tipos de tarea permitidas */
		final Set<Integer> suitable = Set.of(1, 12, 14);
		
		boolean newTask = (suitable.contains(it.getTasca().getTipus().getTipus())) && it.getEstat().equals("E") && 
						   (((Usuari)auth.getPrincipal()).isAdmin() || ((Usuari)auth.getPrincipal()).isGranted());
		
		if(suitable.contains(it.getTasca().getTipus().getTipus())) {   // Evidencia iterable
			String fileName = "";
			
			if(evidencias.size() > 1) {
				fileName = (newTask ? it.getIdInstanciaTasca().add(new BigInteger("1")).toString() : it.getIdInstanciaTasca().toString()) + ".zip";
				try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(filePath+fileName))) {
				    for(MultipartFile f : evidencias) {
				    	File tempFile = File.createTempFile("saic", "temp");
				    	tempFile.deleteOnExit();
				    	f.transferTo(tempFile);
				        zipOut.putNextEntry(new ZipEntry(f.getOriginalFilename()));
				        Files.copy(Paths.get(tempFile.getAbsolutePath()), zipOut);
				    }
				}		
			}
			else if(evidencias.size() == 1){
				MultipartFile evidencia = evidencias.get(0);
				fileName = (newTask ? it.getIdInstanciaTasca().add(new BigInteger("1")).toString() : it.getIdInstanciaTasca().toString()) + "." +
						FilenameUtils.getExtension(evidencia.getOriginalFilename());
				evidencia.transferTo(new File(filePath+fileName));
			}
			else {
				return "0";
			}
			
			if(newTask){
				InstanciaTasca itNew = new InstanciaTasca();
				itNew.setUsuari((Usuari)auth.getPrincipal());
				itNew.setDataFet(new Date(System.currentTimeMillis()));
				itNew.setData(new Date(System.currentTimeMillis()));
				itNew.setEstat("E");
				itNew.setEvidencia(fileName);
				itNew.setInstancia(it.getInstancia());
				itNew.setTasca(it.getTasca());
				itNew.setVersion(it.getVersion()+1);
				itNew.setIdInstanciaTasca(it.getIdInstanciaTasca().add(new BigInteger("1")));
				its.save(itNew);
			}
			else {
				it.setEvidencia(fileName);
				it.setEstat("E");
				this.saveChanges(model, auth, session, it);
			}	
		}
		
		return Integer.toString(evidencias.size()); 
	}
	
	/*
	 * Updates a task instance
	 * 
	 * @param model 
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id Instancia ID Instance to load
	 * @param params Form parameters
	 * @param evidencias List of evidence files (if any)
	 * @return The name of the view to render
	 */
	@PostMapping("/procedure/{id}")
	public String updateInstanciaTasca(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id, @RequestParam Map<String,String> params, 
			@RequestParam(required = false) List<MultipartFile> evidencias) throws IllegalStateException, IOException, InterruptedException {
		
		session.setAttribute("location", "procedures");
		model.addAttribute("location", "procedures");
	
		// Update task instance 
		InstanciaTasca it = its.findById(new BigInteger(params.get("taskid")));
		if(it.getTasca().getTipus().getTipus() == 4) { // Avanzar
			it.setEstat("F");
		}
		else if(it.getTasca().getTipus().getTipus() == 2) { // SI/NO
			if(params.get("response").equals("0")) {
				it.setEstat("N");
			}
			else {
				it.setEstat("S");
			}
		}
		else if(it.getTasca().getTipus().getTipus() == 11 || it.getTasca().getTipus().getTipus() == 15) { // Evidencias online
			it.setText(params.get("evidencia_text"));
			PdfDTO pdf = new PdfDTO(params.get("evidencia_text"), it.getIdInstanciaTasca());
			it.setEvidencia(this.plc.savePDF(pdf));
			it.setEstat("E");
		}

		this.saveChanges(model, auth, session, it);
		this.loadProcedure(model, auth, id);
		return "procedure"; 
	}
	
	/*
	 * Saves changes to a task instance
	 * 
	 * @param model 
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param it InstanciaTasca to save
	 */
	public void saveChanges(Model model, Authentication auth, HttpSession session, InstanciaTasca it) {	
		this.its.saveChanges((Usuari)auth.getPrincipal(), it);
	}
	

	/*
	 * Saves a draft of a task instance (for specific task types)
	 * 
	 * @param model
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id Instancia ID Instance to load
	 * @param text Text content of the draft
	 * @param manual Whether the save was manually triggered by the user
	 * @return The timestamp of the save operation formatted as a String
	 */
	@PostMapping("/procedure/save/{id}")
	@ResponseBody
	public String saveDraft(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id, @RequestParam String text, 
							@RequestParam boolean manual) {
		if(session.getAttribute("location") != null) {
			model.addAttribute("location", session.getAttribute("location"));
		}
		else {
			session.setAttribute("location", "procedures");
			model.addAttribute("location", "procedures");
		}
		InstanciaTasca it = its.findById(id);
		it.setText(text);
		it.setUsuari((Usuari)auth.getPrincipal());
		it.setDataMod(new Date(System.currentTimeMillis()));
		its.save(it);
		if(manual){
			List<InstanciaTascaVer> itvs = this.itsver.findByIdInstanciaTasca(id);
			InstanciaTascaVer itv = new InstanciaTascaVer(id, it.getUsuari().getUsuari(), it.getText(), new Timestamp(it.getDataMod().getTime()));
			if(itvs != null){
				if(itvs.size() == 10){
					this.itsver.delete(itvs.get(itvs.size()-1));
				}
			}
			this.itsver.save(itv);
		}
		
		return String.format("%1$td/%1$tm/%1$tY %1$tH:%1$tM:%1$tS", it.getDataMod());
	}
	
	/*
	 * Loads a procedure instance and its tasks into the model
	 * @param model
	 * @param auth Authentication
	 * @param id Instancia ID Instance to load
	 * @return The name of the view to render
	 */
	public void loadProcedure(Model model, Authentication auth, BigInteger id) {
		Instancia i = is.findByID(id);
		InstanciaTransfer inst = new InstanciaTransfer();
		inst.setIdInstancia(i.getIdInstancia());
		inst.setIdProces(i.getProces().getIdProces());
		inst.setNomProces(i.getProces().getNomProces());
		inst.setCursAvaluat(i.getProces().getCursAvaluat());
		inst.setCursActivacio(i.getProces().getCursActivacio());
		inst.setTitolCas(i.getProces().getTitolCas());
		inst.setTitolVal(i.getProces().getTitolVal());
		inst.setDescripcioCas(i.getProces().getDescripcioCas());
		inst.setDescripcioVal(i.getProces().getDescripcioVal());
		inst.setEstat(i.getEstat());
		if(i.getOrgan().getId().getTlugar().equals("C")) {
			Organ o = ors.findByID("T", i.getTitulacio());
			inst.setTitulacioCas(o.getNomCas());
			inst.setTitulacioVal(o.getNomVal());
			inst.setCentreCas(i.getOrgan().getNomCas());
			inst.setCentreVal(i.getOrgan().getNomVal());
		}
		else {
			inst.setTitulacioCas(i.getOrgan().getNomCas());
			inst.setTitulacioVal(i.getOrgan().getNomVal());
			inst.setCentreCas(i.getOrgan().getOrgan().getNomCas());
			inst.setCentreVal(i.getOrgan().getOrgan().getNomVal());
			inst.setRuct(i.getOrgan().getRuct());
		}
		model.addAttribute("instance", inst);
		
		List<InstanciaTascaTransfer> tasks = new ArrayList<InstanciaTascaTransfer>();
		Boolean active;
		for(InstanciaTasca it : its.findByInstancia(i.getIdInstancia())) {
			active = false;
			InstanciaTascaTransfer itt = new InstanciaTascaTransfer();
			itt.setIdInstanciaTasca(it.getIdInstanciaTasca());
			itt.setIdTasca(it.getTasca().getIdTasca());
			itt.setIdProces(it.getTasca().getProces().getIdProces());
			itt.setTitolCas(it.getTasca().getTitolCas());
			itt.setTitolVal(it.getTasca().getTitolVal());
			itt.setDescripcioCas(it.getTasca().getDescripcioCas());
			itt.setDescripcioVal(it.getTasca().getDescripcioVal());
			itt.setTipus(it.getTasca().getTipus().getTipus());
			itt.setDataLim(it.getTasca().getDataLim());
			itt.setEstat(it.getEstat());
			itt.setIsExpired(it.getTasca().isExpired());
			itt.setIdTascap(it.getTasca().getIdTascap());
			itt.setDescripcioRolCas(it.getTasca().getRol().getDescripcioCas());
			itt.setDescripcioRolVal(it.getTasca().getRol().getDescripcioVal());
			itt.setDataFet(it.getDataFet());
			itt.setEstatInstancia(it.getInstancia().getEstat());
			itt.setOpcions(it.getTasca().getOpcions());
			itt.setDataMod(it.getDataMod());
			
			if(itt.getEstat() != null) {
				if(itt.getEstat().equals("A")) {
					active = true;
				}
			}
			
			if(it.getTasca().getTipus().getTipus() == 11 || it.getTasca().getTipus().getTipus() == 15) {
				Plantilla p = plc.findByID(Integer.parseInt(it.getTasca().getCodiEvidencia()));
				Plantilla p2 = null;
				
				if(i.getTitulacio() > 0) {
					if(!p.getAmbit().equals(i.getOrgan().getTambit())) {
						p2 = plc.findByVersioCodiAmbit(p.getVersio(), p.getCodi(), i.getOrgan().getTambit());
					}
				}			
				
				if(p2 != null) {
					p = p2;
				}
						
				itt.setCodiEvidencia(p.getCodi());
				itt.setNomEvidenciaCas(p.getNomCas());
				itt.setNomEvidenciaVal(p.getNomVal());
				/* Comprobar estado evidencia, si vacío inyectar contenido de plantilla asociada */
				if(it.getText() == null && active) {
					InstanciaTascaDTO itDTO = new InstanciaTascaDTO(it);
					TemplateDataDTO td = new TemplateDataDTO(itDTO, p.getText());
					itt.setText(plc.addTemplateData(td));
				}
				else if(active){
					itt.setText(it.getText());
				}
			}
			else {
				itt.setCodiEvidencia(it.getTasca().getCodiEvidencia());
				itt.setNomEvidenciaCas(it.getTasca().getNomEvidenciaCas());
				itt.setNomEvidenciaVal(it.getTasca().getNomEvidenciaVal());
				itt.setEvidencia(it.getEvidencia());
			}
			
			if(it.getUsuari() != null) {
				itt.setUsuariFet(it.getUsuari().getNom() + " " + it.getUsuari().getCognoms());
			}
			else {
				itt.setUsuariFet("");
			}			
			itt.setAssignedToUser(its.isUserAuthorized(((Usuari)auth.getPrincipal()), itt.getIdInstanciaTasca()));
			itt.setOldEvidences(its.findOlderByProces(it.getInstancia().getCentre(), it.getInstancia().getTitulacio(), it.getInstancia().getProces().getNomProces(), i.getProces().getCursActivacio()));
			itt.setVersions(its.findOlderVersions(it.getInstancia().getIdInstancia(), it.getTasca().getIdTasca(), it.getVersion()));
			tasks.add(itt);
		}
		model.addAttribute("tasks", tasks);
		model.addAttribute("flow", this.getFlowDiagram(i.getProces()));
	}

	/*
	 * Get all drafts for a given task instance
	 * @param model
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id InstanciaTasca ID to load drafts for
	 * @return The name of the view to render
	 */
	@GetMapping("/procedure/drafts/{id}")
	public String getDrafts(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id) {
		List<InstanciaTascaVer> itvs = this.itsver.findByIdInstanciaTasca(id);
		model.addAttribute("versions", itvs);
		model.addAttribute("idInstanciaTasca", id);
		return "procedure_versions";
	}

	/*
	 * Get a specific draft for a given task instance
	 * @param model
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id InstanciaTasca ID to load drafts for
	 * @param dataMod Timestamp of the draft to load
	 * @return The InstanciaTascaVer object representing the draft
	 */
	@GetMapping("/procedure/draft/{id}")
	@ResponseBody
	public InstanciaTascaVer getDraft(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id, @RequestParam Timestamp dataMod) {
		InstanciaTascaVer itv = this.itsver.findById(id, dataMod);
		return itv;
	}

	/*
	 * Restore a specific draft for a given task instance
	 * @param model
	 * @param auth Authentication
	 * @param session HttpSession
	 * @param id InstanciaTasca ID to restore draft for
	 * @param dataMod Timestamp of the draft to restore
	 * @return "1" if successful, "0" otherwise
	 */
	@PostMapping("/procedure/draft/{id}")
	@ResponseBody
	public String restoreDraft(Model model, Authentication auth, HttpSession session, @PathVariable BigInteger id, @RequestParam Timestamp dataMod) {
		try{
			InstanciaTascaVer itv = this.itsver.findById(id, dataMod);
			InstanciaTasca it = this.its.findById(id);
			it.setText(itv.getText());
			this.its.save(it);
		}
		catch(Exception e){
			return "0";
		}

		return "1";
	}

	/*
	 * Generate a flow diagram for a given process
	 * @param proces The process to generate the flow diagram for
	 * @return A String containing the flow diagram
	 */
	private String getFlowDiagram(Proces proces){
		List<String> flow = this.procesService.getFlowDiagram(proces);
		return "graph TD \n" + String.join("\n", flow);
	}
	
}