/* © SRSoftware 2025 */
package de.srsoftware.umbrella.files;

import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.*;
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Path.PROJECT;
import static de.srsoftware.umbrella.core.constants.Text.LONG;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.core.model.Translatable.t;
import static de.srsoftware.umbrella.files.Constants.*;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.text.MessageFormat.format;

import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.BaseHandler;
import de.srsoftware.umbrella.core.ModuleRegistry;
import de.srsoftware.umbrella.core.api.FileService;
import de.srsoftware.umbrella.core.constants.Path;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.Company;
import de.srsoftware.umbrella.core.model.Project;
import de.srsoftware.umbrella.core.model.Token;
import de.srsoftware.umbrella.core.model.UmbrellaUser;
import java.io.*;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

public class FileModule extends BaseHandler implements FileService {

	private final File baseDir;
	private final FileDb fileDb;

	public FileModule(Configuration config) throws UmbrellaException {
		super();
		var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE));
		var filestore = config.get(CONFIG_FILESTORE).orElseThrow(() -> missingField(CONFIG_FILESTORE));
		baseDir = new File(filestore.toString());
		if (!baseDir.exists()) try {
			Files.createDirectories(baseDir.toPath());
		} catch (IOException e) {
			throw unprocessable("Failed to create {location}", LOCATION,baseDir);
		}
		if (!baseDir.isDirectory()) throw unprocessable("{location} is not a directory!", LOCATION,baseDir);
		fileDb = new SqliteDb(connect(dbFile));
		ModuleRegistry.add(this);
	}

	@Override
	public boolean doDelete(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		try {
			Optional<Token> token = SessionToken.from(ex).map(Token::of);
			var user = userService().loadUser(token);
			if (user.isEmpty()) return unauthorized(ex);
			var head = path.pop();
			return switch (head){
				case Path.COMPANY -> deleteCompanyFile(path, ex, user.get());
				case PROJECT -> deleteProjectFile(path, ex, user.get());
				case Path.USER -> deleteUserFile(path, ex, user.get());
				case null, default -> throw UmbrellaException.notFound("invalid location: {location}", LOCATION, head);
			};
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	private boolean deleteCompanyFile(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var cpId = path.pop();
		var companies = companyService();
		Map<Long, Company> companyList;
		if (cpId == null){
			companyList = companies.listCompaniesOf(user);
			var map = companyList.values().stream().collect(Collectors.toMap(company -> "/company/"+company.id(), Company::name));
			return sendContent(ex,Map.of("dirs",map));
		}

		long cid;
		try {
			cid = Long.parseLong(cpId);
		} catch (NumberFormatException e) {
			throw invalidField(COMPANY_ID,LONG);
		}

		var company = companies.get(cid);
		var filename = "/company/"+cid;
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!companies.membership(cid,user.id()) && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		return deleteFile(ex, new File(baseDir+filename));
	}

	private boolean deleteProjectFile(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var prjId = path.pop();
		var projects = projectService();
		if (prjId == null){
			var projectList = projects.listUserProjects(user.id(),true);
			var map = projectList.values().stream().collect(Collectors.toMap(prj -> "/project/"+prj.id(),Project::name));
			return sendContent(ex,Map.of("dirs",map));
		}

		long pid;
		try {
			pid = Long.parseLong(prjId);
		} catch (NumberFormatException e) {
			throw invalidField(PROJECT_ID,t(LONG));
		}
		var project = projects.loadMembers(projects.load(pid));
		var filename = "/project/"+pid;
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!project.hasMember(user) && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		return deleteFile(ex, new File(baseDir+filename));
	}

	private static boolean dropDir(File dir) {
		File[] nested = dir.listFiles();
		if (nested != null) {
			for (File file : nested)  {
				if (!dropDir(file)) return false;
			}
		}
		return dir.delete();
	}

	private boolean deleteFile(HttpExchange ex, File file) throws IOException {
		if (!file.exists()) throw unprocessable("{file} does not exist!",FILE,file.getName());
		if (!dropDir(file)) return sendContent(ex,HTTP_SERVER_ERROR,format("Failed to delete {0}",file.getName()));
		Map<String,Object> map = getDirectory(file.getParentFile());
		map.put("title",file.getName());
		map.put("delete",true);
		return sendContent(ex,map);

	}

	private boolean deleteUserFile(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var userId = path.pop();
		if (userId == null) throw missingField(USER_ID);
		long uid;
		try {
			uid = Long.parseLong(userId);
		} catch (NumberFormatException e) {
			throw invalidField(PROJECT_ID,t(LONG));
		}
		var filename = "/user/"+uid;
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (uid != user.id() && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		return deleteFile(ex, new File(baseDir+filename));
	}

	@Override
	public boolean doGet(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		try {
			Optional<Token> token = SessionToken.from(ex).map(Token::of);
			var user = userService().loadUser(token);
			if (user.isEmpty()) return unauthorized(ex);
			var head = path.pop();
			return switch (head){
				case Path.COMPANY -> getCompanyFiles(path, ex, user.get());
				case PROJECT -> getProjectFiles(path, ex, user.get());
				case Path.USER -> getUserFiles(path, ex, user.get());
				case null, default -> throw UmbrellaException.notFound("invalid location: {location}", LOCATION, head);
			};
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	@Override
	public boolean doPost(de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		addCors(ex);
		try {
			Optional<Token> token = SessionToken.from(ex).map(Token::of);
			var user = userService().loadUser(token);
			if (user.isEmpty()) return unauthorized(ex);
			var head = path.pop();
			return switch (head){
				case Path.COMPANY -> postCompanyDirectory(path, ex, user.get());
				case PROJECT -> postProjectDirectory(path, ex, user.get());
				case Path.USER -> postUserDirectory(path, ex, user.get());
				case null, default -> throw UmbrellaException.notFound("invalid location: {location}", LOCATION, head);
			};
		} catch (UmbrellaException e) {
			return send(ex,e);
		}
	}

	private boolean doPost(HttpExchange ex, String filename) throws IOException {
		var file = new File(baseDir+filename);
		if (contentType(ex).isPresent()) { // file upload
			var out = new FileOutputStream(file);
			var body = ex.getRequestBody();
			body.transferTo(out);
			body.close();
			out.close();
		} else {
			if (file.exists()) throw unprocessable("{file} already exists!",FILE, filename);
			try {
				file.mkdirs();
			} catch (Exception e) {
				throw unprocessable("Failed to create {file}", FILE, filename);
			}
		}
		Map<String,Object> map = getDirectory(file.getParentFile());
		map.put("title",filename);
		return sendContent(ex,map);
	}

	private boolean getCompanyFiles(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var cpId = path.pop();
		var companies = companyService();
		Map<Long, Company> companyList;
		if (cpId == null){
			companyList = companies.listCompaniesOf(user);
			var map = companyList.values().stream().collect(Collectors.toMap(company -> "/company/"+company.id(), Company::name));
			return sendContent(ex,Map.of("dirs",map));
		}

		long cid;
		try {
			cid = Long.parseLong(cpId);
		} catch (NumberFormatException e) {
			throw invalidField(COMPANY_ID,LONG);
		}

		var company = companies.get(cid);
		var companyDir = "/company/"+cid;
		var filename = path.empty() ? companyDir : companyDir + "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!companies.membership(cid,user.id()) && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}", FILE, filename);
		var file = new File(baseDir+filename);
		if (!file.exists()){
			if (!filename.equals(companyDir)) throw unprocessable("{file} does not exist!", FILE, filename);
			if (!file.mkdirs()) throw unprocessable("Failed to create directory {name}!", NAME,filename);
		}
		if (file.isDirectory()){
			Map<String,Object> map = getDirectory(file);
			map.put("title",filename.replace("/company/"+cpId,company.name()));
			map.put("delete",true);
			return sendContent(ex,map);
		}
		return getFile(ex, file);
	}

	private Map<String,Object> getDirectory(File file) throws IOException {
		var children = file.listFiles();
		var map = new HashMap<String, Object>();
		if (children == null) return map;
		var prefixLen = baseDir.toString().length();
		for (var child : children){
			var o = map.computeIfAbsent(child.isDirectory() ? "dirs" : "files", k -> new HashMap<String, String>());
			//noinspection unchecked
			((Map<String,String>) o).put(child.toString().substring(prefixLen),child.getName());
		}
		return map;
	}

	private boolean getFile(HttpExchange ex, File file) throws IOException {
		var headers  = ex.getResponseHeaders();
		var conn = file.toURI().toURL().openConnection();
		var ct = conn.getContentType();
		headers.add(CONTENT_TYPE, ct);
		headers.add(CONTENT_DISPOSITION,"attachment; filename=\""+file.getName()+"\"");
		ex.sendResponseHeaders(HTTP_OK, 0L);
		try (var fos = new FileInputStream(file)){
			fos.transferTo(ex.getResponseBody());
		}
		return true;

	}

	private boolean getProjectFiles(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var prjId = path.pop();
		var projects = projectService();
		if (prjId == null){
			var projectList = projects.listUserProjects(user.id(),true);
			var map = projectList.values().stream().collect(Collectors.toMap(prj -> "/project/"+prj.id(),Project::name));
			return sendContent(ex,Map.of("dirs",map));
		}

		long pid;
		try {
			pid = Long.parseLong(prjId);
		} catch (NumberFormatException e) {
			throw invalidField(PROJECT_ID,t(LONG));
		}
		var project = projects.loadMembers(projects.load(pid));
		var projectDir = "/project/"+pid;
		var filename = path.empty() ? projectDir : projectDir+"/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!project.hasMember(user) && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}", FILE,filename);
		var file = new File(baseDir+filename);
		if (!file.exists()){
			if (!filename.equals(projectDir)) throw unprocessable("{file} does not exist!",FILE,filename);
			if (!file.mkdirs()) throw unprocessable("Failed to create directory {name}!", NAME,filename);
		}
		if (file.isDirectory()){
			Map<String,Object> map = getDirectory(file);
			map.put("title",filename.replace("/project/"+prjId,project.name()));
			map.put("delete",true);
			return sendContent(ex,map);
		}
		return getFile(ex, file);
	}

	private boolean getUserFiles(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var userId = path.pop();
		if (userId == null) throw missingField(USER_ID);
		long uid;
		try {
			uid = Long.parseLong(userId);
		} catch (NumberFormatException e) {
			throw invalidField(PROJECT_ID,t(LONG));
		}
		var userDir = "/user/"+uid;
		var filename = path.empty() ? userDir : userDir+"/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (uid != user.id() && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		var file = new File(baseDir+filename);
		if (!file.exists()){
			if (!filename.equals(userDir)) throw unprocessable("{file} does not exist!",FILE,filename);
			if (!file.mkdirs()) throw unprocessable("Failed to create directory {name}!", NAME,filename);
		}		if (file.isDirectory()){
			Map<String,Object> map = getDirectory(file);
			map.put("title",filename);
			map.put("delete",true);
			return sendContent(ex,map);
		}
		return getFile(ex, file);
	}

	private boolean postCompanyDirectory(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var cpId = path.pop();
		var companies = companyService();
		Map<Long, Company> companyList;
		if (cpId == null){
			companyList = companies.listCompaniesOf(user);
			var map = companyList.values().stream().collect(Collectors.toMap(company -> "/company/"+company.id(), Company::name));
			return sendContent(ex,Map.of("dirs",map));
		}

		long cid;
		try {
			cid = Long.parseLong(cpId);
		} catch (NumberFormatException e) {
			throw invalidField(COMPANY_ID,LONG);
		}

		var filename = "/company/"+cid;
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!companies.membership(cid,user.id()) && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		return doPost(ex,filename);
	}

	private boolean postProjectDirectory(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var prjId = path.pop();
		var projects = projectService();
		if (prjId == null){
			var projectList = projects.listUserProjects(user.id(),true);
			var map = projectList.values().stream().collect(Collectors.toMap(prj -> "/project/"+prj.id(),Project::name));
			return sendContent(ex,Map.of("dirs",map));
		}

		long pid;
		try {
			pid = Long.parseLong(prjId);
		} catch (NumberFormatException e) {
			throw invalidField(PROJECT_ID,t(LONG));
		}
		var project = projects.loadMembers(projects.load(pid));
		var filename = "/project/"+pid;
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (!project.hasMember(user) && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		return doPost(ex,filename);
	}

	private boolean postUserDirectory(de.srsoftware.tools.Path path, HttpExchange ex, UmbrellaUser user) throws IOException {
		var userId = path.pop();
		if (userId == null) throw missingField(USER_ID);
		long uid;
		try {
			uid = Long.parseLong(userId);
		} catch (NumberFormatException e) {
			throw invalidField(PROJECT_ID,t(LONG));
		}
		var filename = "/user/"+uid;
		if (!path.empty()) filename += "/"+URLDecoder.decode(path.toString(),UTF_8);
		if (uid != user.id() && !fileDb.isPermitted(user,filename)) throw forbidden("You are not allowed to access {file}",FILE,filename);
		return doPost(ex,filename);
	}
}
