package org.codefilarete.stalactite.dsl.naming;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Comparator;

import org.codefilarete.stalactite.query.model.JoinLink;
import org.codefilarete.stalactite.sql.ddl.structure.Column;
import org.codefilarete.stalactite.sql.ddl.structure.Key;
import org.codefilarete.stalactite.sql.ddl.structure.Table;
import org.codefilarete.tool.exception.Exceptions;
import org.codefilarete.tool.trace.MutableInt;

import static org.codefilarete.tool.collection.Iterables.first;

/**
 * Contract for giving a name to a foreign key
 *
 * @author Guillaume Mary
 */
public interface ForeignKeyNamingStrategy {
	
	String DEFAULT_FOREIGNKEY_PREFIX = "FK_";
	
	Comparator<JoinLink<?, ?>> COLUMN_COMPARATOR = Comparator.comparing(JoinLink::getExpression);
	
	<SOURCETABLE extends Table<SOURCETABLE>, TARGETTABLE extends Table<TARGETTABLE>, ID> String giveName(Key<SOURCETABLE, ID> src, Key<TARGETTABLE, ID> target);
	
	ForeignKeyNamingStrategy HASH = new ForeignKeyNamingStrategy() {
		@Override
		public <SOURCETABLE extends Table<SOURCETABLE>, TARGETTABLE extends Table<TARGETTABLE>, ID> String giveName(Key<SOURCETABLE, ID> src, Key<TARGETTABLE, ID> target) {
			MutableInt hashCode = new MutableInt(src.getTable().getAbsoluteName().hashCode());
			hashCode.reset(hashCode.getValue() * 31 + target.getTable().getAbsoluteName().hashCode());
			// We ensure a consistent ordering of columns, regardless of the order they were bound.
			src.getColumns().stream().sorted(COLUMN_COMPARATOR).forEach(joinLink -> {
				hashCode.reset(hashCode.getValue() * 31 + joinLink.getExpression().hashCode());
			});
			target.getColumns().stream().sorted(COLUMN_COMPARATOR).forEach(joinLink -> {
				hashCode.reset(hashCode.getValue() * 31 + joinLink.getExpression().hashCode());
			});
			return DEFAULT_FOREIGNKEY_PREFIX + Integer.toHexString(hashCode.getValue());
		}
	};
	
	/**
	 * Composed of a prefix, source table name, source column name, target column name.
	 * Must be quite unique to prevent Database such as HSQLDB to yel because key names uniqueness is over a table space, not per table.
	 */
	ForeignKeyNamingStrategy DEFAULT = new ForeignKeyNamingStrategy() {
		@Override
		public <SOURCETABLE extends Table<SOURCETABLE>, TARGETTABLE extends Table<TARGETTABLE>, ID> String giveName(Key<SOURCETABLE, ID> src, Key<TARGETTABLE, ID> target) {
			if (src.getColumns().size() == 1) {
				return DEFAULT_FOREIGNKEY_PREFIX + src.getTable().getName() + "_" + ((Column) first(src.getColumns())).getName()
						+ "_" + target.getTable().getName() + "_" + ((Column) first(target.getColumns())).getName();
			} else {
				return HASH.giveName(src, target);
			}
		}
	};
	
	/**
	 * Generates same name as Hibernate (4.3.7) does. From org.hibernate.mapping.Constraint#generateName(String, Table, Column... columns)
	 */
	ForeignKeyNamingStrategy HIBERNATE_4 = new ForeignKeyNamingStrategy() {
		@Override
		public <SOURCETABLE extends Table<SOURCETABLE>, TARGETTABLE extends Table<TARGETTABLE>, ID> String giveName(Key<SOURCETABLE, ID> src, Key<TARGETTABLE, ID> target) {
			// Use a concatenation that guarantees uniqueness, even if identical names
			// exist between all table and column identifiers.
			StringBuilder sb = new StringBuilder("table`" + src.getTable().getName() + "`");
			
			// We ensure a consistent ordering of columns, regardless of the order they were bound.
			target.getColumns().stream().sorted(COLUMN_COMPARATOR).forEach(column -> sb.append("column`").append(column.getExpression()).append("`"));
			return "FK" + hashName(sb.toString());
		}
	};
	
	/**
	 * Generates a hash for a foreign key represented by given arguments
	 * Implementation that returns the same hash as Hibernate 7 does.
	 * From org.hibernate.boot.model.naming.NamingHelper#generateHashedFkName(String, Identifier, Identifier, List&lt;Identifier&gt;...)
	 */
	ForeignKeyNamingStrategy HIBERNATE_7 = new ForeignKeyNamingStrategy() {
		@Override
		public <SOURCETABLE extends Table<SOURCETABLE>, TARGETTABLE extends Table<TARGETTABLE>, ID> String giveName(Key<SOURCETABLE, ID> src, Key<TARGETTABLE, ID> target) {
			// Use a concatenation that guarantees uniqueness, even if identical names
			// exist between all table and column identifiers.
			StringBuilder sb = new StringBuilder("table`" + src.getTable().getName() + "`")
					.append("references`").append(target.getTable().getName()).append("`");
			
			// Note that referenced columns are not taken into account because the primary key is automatically targeted by database engines (same
			// principle as Hibernate 7).
			// We ensure a consistent ordering of columns, regardless of the order they were bound.
			src.getColumns().stream().sorted(COLUMN_COMPARATOR).forEach(column -> sb.append("column`").append(column.getExpression()).append("`"));
			return "FK" + hashName(sb.toString());
		}
	};
	
	/**
	 * Same algorithm as Hibernate (4.3.7), see org.hibernate.mapping.Constraint#hashedName(String)
	 *
	 * @param s the string to be hashed
	 * @return a hashed (MD5) version of the given String, less than 30 characters
	 */
	static String hashName(String s) {
		try {
			@SuppressWarnings("java:S4790" /* No sensitive code related to MD5 usage : only used to hash foreign key name */)
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.update(s.getBytes());
			byte[] digest = md.digest();
			BigInteger bigInt = new BigInteger(1, digest);
			// By converting to base 35 (full alphanumeric), we guarantee
			// that the length of the name will always be smaller than the 30
			// character identifier restriction enforced by a few dialects.
			return bigInt.toString(35);
		} catch (NoSuchAlgorithmException e) {
			throw Exceptions.asRuntimeException(e);
		}
	}
}
