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

import static de.srsoftware.tools.Optionals.is0;
import static de.srsoftware.tools.Optionals.nullIfEmpty;
import static de.srsoftware.umbrella.core.ConnectionProvider.connect;
import static de.srsoftware.umbrella.core.ModuleRegistry.companyService;
import static de.srsoftware.umbrella.core.ModuleRegistry.userService;
import static de.srsoftware.umbrella.core.Util.mapValues;
import static de.srsoftware.umbrella.core.constants.Field.*;
import static de.srsoftware.umbrella.core.constants.Field.ITEM;
import static de.srsoftware.umbrella.core.constants.Module.COMPANY;
import static de.srsoftware.umbrella.core.constants.Module.USER;
import static de.srsoftware.umbrella.core.constants.Path.*;
import static de.srsoftware.umbrella.core.constants.Path.PROPERTY;
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
import static de.srsoftware.umbrella.messagebus.MessageBus.messageBus;
import static de.srsoftware.umbrella.stock.Constants.*;
import static java.lang.System.Logger.Level.WARNING;
import static java.util.Comparator.comparing;

import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.configuration.Configuration;
import de.srsoftware.configuration.JsonConfig;
import de.srsoftware.tools.SessionToken;
import de.srsoftware.umbrella.core.*;
import de.srsoftware.umbrella.core.api.Owner;
import de.srsoftware.umbrella.core.api.StockService;
import de.srsoftware.umbrella.core.constants.Field;
import de.srsoftware.umbrella.core.constants.Module;
import de.srsoftware.umbrella.core.constants.Path;
import de.srsoftware.umbrella.core.constants.Text;
import de.srsoftware.umbrella.core.exceptions.UmbrellaException;
import de.srsoftware.umbrella.core.model.*;
import de.srsoftware.umbrella.core.model.Location;
import de.srsoftware.umbrella.messagebus.events.Event;
import de.srsoftware.umbrella.messagebus.events.ItemEvent;
import java.io.IOException;
import java.util.*;
import org.json.JSONObject;

public class StockModule extends BaseHandler implements StockService {

	private final StockDb stockDb;
	private Comparator<Item> byName = (a,b) -> a.name().compareToIgnoreCase(b.name());

	public StockModule(Configuration config) throws UmbrellaException {
		super();
		var dbFile = config.get(CONFIG_DATABASE).orElseThrow(() -> missingField(CONFIG_DATABASE));
		stockDb = new SqliteDb(connect(dbFile));
		Optional<String> itemDbConfig = config.get(CONFIG_ITEM_DB);
		itemDbConfig.map(ItemDb::new).ifPresent(itemDb -> itemDb.migrateTo(stockDb));
		if (itemDbConfig.isPresent()){
			try {
				config.drop(CONFIG_ITEM_DB);
				if (config instanceof JsonConfig jsonConfig) jsonConfig.save();
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
		ModuleRegistry.add(this);
	}

	private boolean assigned(Owner owner, UmbrellaUser user){
		owner = owner.resolve();
		if (owner instanceof UmbrellaUser u && user.id() == u.id()) return true;
		if (owner instanceof Company comp) return companyService().membership(comp.id(),user.id());
		return false;
	}

	private boolean deleteLocation(UmbrellaUser user, Location locationRef, HttpExchange ex) throws IOException {
		var location = locationRef.resolve();
		var owner = location.owner().resolve();
		if (!assigned(owner,user)) throw forbidden("You are not allowed to modify \"{location}\"", Field.LOCATION,location);
		if (!stockDb.listItemsAt(location).isEmpty()) throw forbidden("\"{location}\" cannot be deleted, as it contains items!", Field.LOCATION,location);
		if (!stockDb.listChildLocations(location.id()).isEmpty()) throw forbidden("\"{location}\" cannot be deleted, as it contains other locations!", Field.LOCATION,location);
		return sendContent(ex,stockDb.delete(location));
	}

	@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.LOCATION -> {
					try {
						var location = Location.of(Long.parseLong(path.pop()));
						yield deleteLocation(user.get(), location, ex);
					} catch (NumberFormatException e){
						yield super.doGet(path,ex);
					}
				}
				case null, default -> super.doDelete(path,ex);
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	@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 -> {
					try {
						var company = companyService().get(Long.parseLong(path.pop()));
						yield getItem(user.get(),company,path,ex);
					} catch (NumberFormatException e){
						yield super.doGet(path,ex);
					}
				}
				case Path.ITEM -> getItemById(user.get(),path,ex);
				case Path.LOCATION -> {
					try {
						var location = Location.of(Long.parseLong(path.pop()));
						yield getLocationEntities(location, ex);
					} catch (Exception e){
						yield super.doGet(path,ex);
					}
				}
				case Path.LOCATIONS -> getLocations(path,user.get(),ex);
				case Path.PROPERTIES -> getProperties(ex);
				case Path.USER -> {
					try {
						var userId = Long.parseLong(path.pop());
						if (userId != user.get().id()) throw forbidden("You are not allowed to access items of another user!");
						yield getItem(user.get(),user.get(),path,ex);
					} catch (NumberFormatException e){
						yield super.doGet(path,ex);
					}
				}
				case null -> super.doGet(path,ex);
				default -> {
					try {
						var id = Long.parseLong(head);
						Item item = stockDb.loadItem(id);
						Owner owner = item.owner().resolve();
						if (owner instanceof Company company) {
							if (!companyService().membership(company.id(),user.get().id())) throw forbidden("You are not allowed to access {0}",OBJECT);
						}
						if (owner instanceof UmbrellaUser u){
							if (u.id() != user.get().id()) throw forbidden("You are not allowed to access {0}",OBJECT);
						}
						yield sendContent(ex,item);
				} catch (NumberFormatException nfe){
						yield super.doGet(path,ex);
					}
				}
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	private boolean getItemById(UmbrellaUser user, de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		var head = path.pop();
		if (head == null) throw missingField(Field.ID);
		try {
			var itemId = Long.parseLong(head);
			var item = stockDb.loadItem(itemId);
			var owner = item.location().resolve().owner().resolve();
			boolean allowed = owner instanceof UmbrellaUser u && user.equals(u);
			allowed = allowed || owner instanceof Company c && companyService().membership(c.id(),user.id());
			if (!allowed) throw forbidden("You are not allowed to access item {id}",ID,itemId);
			return sendContent(ex,item);
		} catch (NumberFormatException e) {
			throw invalidField(Field.ID, Text.NUMBER);
		}
	}

	@Override
	public boolean doPatch(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);
			return switch (path.pop()){
				case MOVE_ITEM -> patchMoveItem(user.get(), path,ex);
				case Path.LOCATION -> {
					try {
						var id = Long.parseLong(path.pop());
						yield patchLocation(id, user.get(), ex);
					} catch (NumberFormatException nfe){
						yield super.doPatch(path,ex);
					}

				}
				case null -> patchItem(user.get(),ex);
				default -> super.doPatch(path,ex);
			};
		} 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.CLONE -> postClone(user.get(),ex);
				case Path.ITEM -> postItem(user.get(), ex);
				case LIST     -> postItemList(user.get(), path, ex);
				case Path.LOCATION -> postLocation(user.get(),ex);
				case PROPERTY -> postProperty(user.get(),ex);
				case SEARCH   -> postSearch(user.get(),ex);
				case null, default -> super.doPost(path,ex);
			};
		} catch (UmbrellaException e){
			return send(ex,e);
		}
	}

	private boolean getChildLocations(UmbrellaUser user, long parentId, HttpExchange ex) throws IOException {
		var owner = stockDb.loadLocation(parentId).owner();
		if (!assigned(owner,user)) throw forbidden("You are not allowed to access items of {owner}", OWNER,owner);
		return sendContent(ex, stockDb.listChildLocations(parentId).stream().sorted(comparing(l -> l.name().toLowerCase())).map(DbLocation::toMap));
	}

	private boolean getItem(UmbrellaUser user, Owner owner, de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		if (!assigned(owner,user)) throw forbidden("You are not allowed to access items of {owner}", OWNER,owner);
		return switch (path.pop()){
			case Path.ITEM -> {
				try {
					var itemId = Long.parseLong(path.pop());
					var item   = stockDb.loadItem(owner.dbCode(),itemId);
					yield getLocationEntities(item.location(),ex);
				} catch (NumberFormatException e) {
					yield super.doGet(path,ex);
				}
			}
			case null, default -> super.doGet(path,ex);
		};
	}

	private boolean getLocationEntities(Location location, HttpExchange ex) throws IOException {
		var items = stockDb.listItemsAt(location).stream().map(Item::toMap).toList();
		var owner = location.resolve().owner();
		List<Long> userIds = switch (owner.type()){
			case COMPANY -> companyService().getMembers(owner.id()).stream().map(UmbrellaUser::id).toList();
			case USER -> List.of(owner.id());
			case null, default -> throw unprocessable("Unprocessable owner type: {type}", TYPE,owner.type());
		};
		var pathToLocation = stockDb.pathToLocation(location);
		return sendContent(ex,Map.of(ITEMS,items,USERS,userIds, PATH,pathToLocation, Field.LOCATION,location.resolve().toMap()));
	}

	private boolean getLocations(de.srsoftware.tools.Path path, UmbrellaUser user, HttpExchange ex) throws IOException {
		var head = path.pop();
		return switch (head){
			case BELOW -> {
				try {
					var id = Long.parseLong(path.pop());
					yield getChildLocations(user, id, ex);
				} catch (Exception e){
					yield super.doGet(path,ex);
				}

			}
			case OF_USER -> getUserLocations(user,ex);
			case null, default -> super.doGet(path,ex);
		};
	}

	private boolean getProperties(HttpExchange ex) throws IOException {
		return sendContent(ex,stockDb.listProperties().stream().map(Property::toMap).toList());
	}

	private boolean getUserLocations(UmbrellaUser user, HttpExchange ex) throws IOException {
		var result = new ArrayList<Object>();
		var userLocations = stockDb.listUserLocations(user);
		result.add(Map.of(
				PARENT, Map.of(Field.USER, user.id()),
				NAME,user.name(),
				Field.LOCATIONS,userLocations.stream().map(DbLocation::toMap).toList()));

		var companies = companyService().listCompaniesOf(user);
		companies.values().stream().sorted(comparing(a -> a.name().toLowerCase()))
				.map(company -> Map.of(
						PARENT, Map.of(Field.COMPANY, company.id()),
						NAME,company.name(),
						Field.LOCATIONS,getCompanyLocations(company).stream().sorted(comparing(a -> a.name().toLowerCase())).map(DbLocation::toMap).toList()))
					.forEach(result::add);
		return sendContent(ex, result);
	}

	public Collection<DbLocation> getCompanyLocations(Company company){
		return stockDb.listCompanyLocations(company);
	}

	@Override
	public DbLocation loadLocation(long locationId) {
		return stockDb.loadLocation(locationId);
	}

	private boolean patchItem(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.get(ID) instanceof Number id)) throw missingField(ID);
		json.remove(ID);
		LOG.log(WARNING,"Missing permission check in StockModule.patchItem()!");
		var item = stockDb.loadItem(id.longValue());
		item.patch(json);
		return sendContent(ex,stockDb.save(item));
	}

	private boolean patchMoveItem(UmbrellaUser user, de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.get(ITEM) instanceof Number itemId)) throw missingField(ITEM);
		if (!(json.get(TARGET) instanceof Number locationId)) throw missingField(TARGET);
		var item = stockDb.loadItem(itemId.longValue());

		var itemOwner = item.owner().resolve();
		if (!assigned(itemOwner,user)) throw forbidden("You are not allowed to alter the location of \"{name}\"!", NAME,item.name());

		var target = stockDb.loadLocation(locationId.longValue());
		var locOwner = target.resolve().owner().resolve();
		if (!assigned(locOwner,user)) throw forbidden("You are not allowed to modify \"{name}\"!", NAME,target.name());

		if (!locOwner.equals(itemOwner)) throw unprocessable("You may not move items from one owner ({owner}) to another ({object})", OWNER,itemOwner, OBJECT,locOwner);
		stockDb.save(item.location(target));

		return sendContent(ex,item);
	}

	private boolean patchLocation(long locationId, UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		var location = stockDb.loadLocation(locationId);
		var owner = location.owner().resolve();
		if (!assigned(owner,user)) throw forbidden("You are not allowed to edit \"{location}\"!", Field.LOCATION,location.name());

		if (json.has(PARENT_LOCATION_ID) && json.get(PARENT_LOCATION_ID) instanceof Number parentId){
			if (parentId.longValue() != 0L) {
				var target = stockDb.loadLocation(parentId.longValue());
				if (target.id() == location.id()) throw forbidden("Location must not be it`s own parent!");
				var current = target;
				while (current != null){
					if (current.id() == locationId) throw forbidden("Location cannot be contained in itself!");
					current = is0(current.parent()) ? null : stockDb.loadLocation(current.parent());
				}
				var targetOwner = target.owner().resolve();
				if (!assigned(targetOwner, user)) throw forbidden("You are not allowed to edit \"{name}\"!", NAME, target.name());
				if (!targetOwner.equals(owner))   throw unprocessable("You may not move locations from one owner ({a}) to another ({b})","a", owner,"b", targetOwner);
				LOG.log(WARNING,"Not checking, if location is moved to on of its own children!");
			}
		}

		location = stockDb.save(location.patch(json));
		return sendContent(ex,location);
	}

	private boolean postClone(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!json.has(ID))throw missingField(ID);
		if (!(json.get(ID) instanceof Number num)) throw invalidField(ID,Text.NUMBER);
		long itemId = num.longValue();
		var item     = stockDb.loadItem(itemId);
		stockDb.loadProperties(item);
		var location = item.location().resolve();
		var owner    = location.owner().resolve();
		if (!assigned(owner,user)) throw forbidden("You are not allowed to add items to \"{location}\"!", Text.LOCATION,location.name());
		var newItem = new Item(0,owner,0,location,item.code(),item.name(),item.description());
		for (var property : item.properties()) newItem.properties().add(property);
		newItem = stockDb.save(newItem);
		messageBus().dispatch(new ItemEvent(user, Module.STOCK, newItem, Event.EventType.CREATE));
		return sendContent(ex,newItem);
	}

	private boolean postItem(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!json.has(NAME) || !(json.get(NAME) instanceof String name)) throw missingField(NAME);
		var description = json.has(DESCRIPTION) && json.get(DESCRIPTION) instanceof String d ? d : null;
		if (!json.has(Field.CODE) || !(json.get(Field.CODE) instanceof String code)) throw missingField(Field.CODE);
		if (!json.has(Field.LOCATION) || !(json.get(Field.LOCATION) instanceof JSONObject locationData)) throw missingField(Field.LOCATION);
		var location = stockDb.loadLocation(locationData.getLong(ID));
		var owner = location.owner().resolve();
		if (!assigned(owner,user)) throw forbidden("You are not allowed to add items to {location}!", Field.LOCATION,location);
		var newItem = stockDb.save(new Item(0,owner,0,location,code,name,description));
		messageBus().dispatch(new ItemEvent(user, Module.STOCK, newItem, Event.EventType.CREATE));
		return sendContent(ex,newItem);

	}

	private boolean postItemList(UmbrellaUser user, de.srsoftware.tools.Path path, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!json.has(COMPANY_ID) || !(json.get(COMPANY_ID) instanceof Number company_id)) throw missingField(COMPANY_ID);
		var company = companyService().get(company_id.longValue());
		if (!companyService().membership(company_id.longValue(),user.id())) throw notAmember(company.name());
		var map = new HashMap<Long,Location>();
		var items = stockDb.listItemsOf(company)
				.stream()
				.peek(item -> item.location(map.computeIfAbsent(item.location().id(), k -> item.location().resolve()))).sorted(byName)
				.map(Item::toMap);
		return sendContent(ex,items);
	}

	private boolean postLocation(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.get(NAME) instanceof String name)) throw missingField(NAME);
		if (!(json.get(PARENT) instanceof JSONObject parentData)) throw missingField(PARENT);
		var key = parentData.keySet().stream().findFirst().orElseThrow(() -> missingField(PARENT));
		if (!(parentData.get(key) instanceof Number id)) throw missingField(key);
		Location parent;
		Owner owner;
		switch (key){
			case COMPANY:
				owner = companyService().get(id.longValue());
				parent = null;
				break;
			case USER:
				owner = userService().loadUser(id.longValue());
				parent = null;
				break;
			case Path.LOCATION:
				parent = stockDb.loadLocation(id.longValue());
				owner = parent.resolve().owner().resolve();
				break;
			default: throw unprocessable("Unknown parent object: {key} → {id}", KEY,key, ID,id);
		};
		var loc = new DbLocation(0,owner,parent == null?null:parent.id(),name,null);
		return sendContent(ex,stockDb.save(loc));
	}

	private boolean postProperty(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.get(ITEM) instanceof JSONObject itemData)) throw missingField(ITEM);
		if (!(itemData.get(ID) instanceof Number itemId)) throw missingField(ID);
		if (!(json.get("add_prop") instanceof JSONObject propData)) throw missingField("add_prop");
		if (!propData.has(VALUE)) throw missingField(VALUE);
		LOG.log(WARNING,"Missing permission check in StockModule.postProperty()!");
		var value = propData.get(VALUE);
		if (value == null) throw missingField(VALUE);

		Property property = null;
		if (propData.get("existing_prop_id") instanceof Number existingPropId && existingPropId.longValue() != 0L){
			property = stockDb.setProperty(itemId.longValue(),existingPropId.longValue(),value);
		} else {
			if (!(propData.get("new_prop") instanceof JSONObject newProp)) throw unprocessable("data must contain either add_prop.existing_prop_id or add_prop.new_prop!");
			if (!(newProp.get(NAME) instanceof String name) || name.isBlank()) throw unprocessable("data.add_prop.new_prop does not contain name!");
			var unit = newProp.get(UNIT) instanceof String u ? nullIfEmpty(u) : null;
			property = stockDb.addNewProperty(itemId.longValue(),name,value,unit);
		}
		return sendContent(ex,property);
	}

	private boolean postSearch(UmbrellaUser user, HttpExchange ex) throws IOException {
		var json = json(ex);
		if (!(json.has(KEY) && json.get(KEY) instanceof String key)) throw missingField(KEY);
		var keys = Arrays.asList(key.split(" "));
		var fulltext = json.has(FULLTEXT) && json.get(FULLTEXT) instanceof Boolean val && val;
		Set<Owner> owners = new HashSet<>(companyService().listCompaniesOf(user).values());
		owners.add(user);
		var items = stockDb.find(owners,keys,fulltext);
		return sendContent(ex,mapValues(items));
	}

	@Override
	public Collection<Item> getCompanyItems(long companyID) {
		return stockDb.listItemsOf(companyService().get(companyID));
	}
}
