package project.svc.generic.db;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Serializable;
import java.sql.Types;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.LogManager;

import common.db.JdbcSource;
import common.db.dao.DaoConstraintException;
import core.config.Factory;
import core.util.NumberUtil;
import core.util.bean.CamelCase;
import online.model.ModelUtil;
import online.model.UniModel;
import online.model.UniModelImpl;
import project.base.upload.ImportAbstract;
import project.common.CheckUtil;
import project.common.CsvUtil;
import project.common.db.DBColumnInfo;
import project.common.db.DBMetaData;
import project.common.master.Msg;

/**
 * インポート更新
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class UploadImport extends ImportAbstract {

	/** 行番号 */
	private static final String ERROR_LINE = "ErrorLine";
	/** 項目名 */
	private static final String ERROR_ITEM = "ErrorItem";
	/** メッセージ */
	private static final String ERROR_MSG = "ErrorMsg";

	/** テーブル名 */
	private EntityInfo mm = null;
	/** 汎用モデル */
	private final UniModel model = new UniModelImpl();
	/** エラー集合 */
	private final Set<String> error = new HashSet<>();
	/** 無視集合 */
	private final Set<String> ignore = new HashSet<>();

	/**
	 * 汎用モデル取得
	 * @return 汎用モデル
	 */
	public UniModel getResultModel() {
		return this.model;
	}

	/**
	 * テーブル名設定
	 * @param val テーブル名
	 */
	public void setTable(final String val) {
		this.mm = new EntityInfo(val);
		setCreatorName(this.mm.getCreatorName());
		setCreatedName(this.mm.getCreatedName());
		setUpdaterName(this.mm.getUpdaterName());
		setUpdatedName(this.mm.getUpdatedName());
		setVersionName(this.mm.getVersionName());
	}

	/**
	 * 無視行設定
	 * @param vals 無視行
	 */
	public void setIgnore(final String... vals) {
		this.ignore.clear();
		this.ignore.addAll(Arrays.asList(vals));
	}

	/**
	 * エラー行設定
	 * @param vals エラー行
	 */
	public void setError(final String... vals) {
		this.error.clear();
		this.error.addAll(Arrays.asList(vals));
	}

	/**
	 * @see project.base.upload.ImportAbstract#check()
	 */
	@Override
	public boolean check() {
		this.model.clear();
		setCount(0);

		final var info = getMetaInfo();
		if (info == null) {
			throw new IllegalStateException(this.mm.getTable());
		}

		try (var is = new BufferedInputStream(getInputStream())) {
			try (var isr = new InputStreamReader(is, getCharset())) {
				return checkFile(isr, info);
			}
		} catch (final IOException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return false;
		}
	}

	/**
	 * @see project.base.upload.ImportAbstract#insert()
	 */
	@Override
	public int insert() {
		this.model.clear();
		setCount(0);

		final var info = getMetaInfo();
		if (info == null) {
			throw new IllegalStateException(this.mm.getTable());
		}

		try (var is = new BufferedInputStream(getInputStream())) {
			try (var isr = new InputStreamReader(is, getCharset())) {
				return insertFile(isr, info);
			}
		} catch (final IOException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return 0;
		}
	}

	/**
	 * テーブル情報取得
	 * @return テーブル情報
	 */
	private Map<String, DBColumnInfo> getMetaInfo() {
		final var meta = Factory.create(DBMetaData.class);
		return meta.getColumnInfo(this.mm.getTable(), getSchema());
	}

	/**
	 * ヘッダ配列化
	 * @param info メタ情報
	 * @param str 文字列
	 * @return ヘッダ配列
	 */
	private String[] toHeader(final Map<String, DBColumnInfo> info, final String str) {
		final var vals = CsvUtil.toArray(str);
		for (final var vl : vals) {
			if (!info.containsKey(vl)) {
				return new String[0];
			}
		}
		return vals;
	}

	/**
	 * ヘッダ取得
	 * @param info メタ情報
	 * @return ヘッダ配列
	 */
	private String[] getHeader(final Map<String, DBColumnInfo> info) {
		return info.keySet().toArray(new String[info.size()]);
	}

	/**
	 * ファイルチェック
	 * @param br リーダ
	 * @param info メタ情報
	 * @return チェック結果
	 */
	private boolean checkFile(final Reader br, final Map<String, DBColumnInfo> info) {
		var str = CsvUtil.readLine(br);
		final var header = toHeader(info, str);
		if (0 < header.length) {
			str = CsvUtil.readLine(br);
		}

		var i = 0;
		var ret = true;
		while (str != null) {
			if (0 < header.length) {
				ret = checkLine(++i, str, info, header) && ret;
			} else {
				ret = checkLine(++i, str, info) && ret;
			}
			str = CsvUtil.readLine(br);
		}

		setCount(i);

		return ret;
	}

	/**
	 * 行チェック
	 * @param line 行番号
	 * @param str 行文字列
	 * @param info メタ情報
	 * @return 正常の場合 true を返す。
	 */
	private boolean checkLine(final int line, final String str,
			final Map<String, DBColumnInfo> info) {
		final var vals = CsvUtil.toArray(str);
		var ret = true;
		var i = 0;
		for (final var me : info.entrySet()) {
			if (i < vals.length) {
				ret = checkValue(line, vals[i++], me.getKey(), me.getValue()) && ret;
			}
		}
		return ret;
	}

	/**
	 * 行チェック
	 * @param line 行番号
	 * @param str 行文字列
	 * @param info メタ情報
	 * @param header ヘッダ配列
	 * @return 正常の場合 true を返す。
	 */
	private boolean checkLine(final int line, final String str,
			final Map<String, DBColumnInfo> info, final String[] header) {
		var ret = true;
		final var vals = CsvUtil.toArray(str);
		for (var i = 0; i < header.length; i++) {
			if (i < vals.length) {
				ret = checkValue(line, vals[i], header[i], info.get(header[i])) && ret;
			}
		}
		return ret;
	}

	/**
	 * 値チェック
	 * @param line 行番号
	 * @param val 値
	 * @param item 項目名
	 * @param info メタ情報
	 * @return 正常の場合 true を返す。
	 */
	private boolean checkValue(final int line, final String val,
			final String item, final DBColumnInfo info) {

		if ("Id".equalsIgnoreCase(item) && val.isEmpty()) {
			return true;
		}

		// 必須入力
		if (info.isNotNull() && val.isEmpty()) {
			addMsg(line, info.getComment(), "ZZ000000006", info.getComment());
			return false;
		}

		var ret = true;
		// タイプと長さ
		if (info.getType() == Types.CHAR) {
			if (!CheckUtil.isHanEisu(val)) {
				addMsg(line, info.getComment(), "ZZ000000007", info.getComment());
				ret = false;
			}
			if (!CheckUtil.isJustByte(val, info.getSize(), getCharset())) {
				addMsg(line, info.getComment(), "ZZ000000008",
								info.getComment(), "", String.valueOf(info.getSize()));
				ret = false;
			}
		} else if (info.getType() == Types.VARCHAR) {
			final var len = Math.min(info.getSize(), Integer.MAX_VALUE - 1);
			if (!CheckUtil.isLessByte(val, len + 1, getCharset())) {
				addMsg(line, info.getComment(), "ZZ000000009", info.getComment());
				ret = false;
			}
		} else if (info.getType() == Types.DATE) {
			if (!CheckUtil.isDateTime(val)) {
				addMsg(line, info.getComment(), "ZZ000000010", info.getComment());
				ret = false;
			}
		} else if (info.getType() == Types.TIMESTAMP) {
			if (!CheckUtil.isDateTime(val)) {
				addMsg(line, info.getComment(), "ZZ000000011", info.getComment());
				ret = false;
			}
		} else {
			if (!CheckUtil.isNumber(val)) {
				addMsg(line, info.getComment(), "ZZ000000012", info.getComment());
				ret = false;
			}
			final var len = Math.min(info.getSize(), Integer.MAX_VALUE - 1);
			if (!CheckUtil.isLessByte(val, len + 1, getCharset())) {
				addMsg(line, info.getComment(), "ZZ000000009", info.getComment());
				ret = false;
			}
		}

		return ret;
	}

	/**
	 * メッセージ追加
	 * @param line 行番号
	 * @param comment コメント
	 * @param mid メッセージID
	 * @param prm メッセージパラメータ
	 */
	private void addMsg(final int line, final String comment,
			final String mid, final String... prm) {
		final var msg = Factory.create(Msg.class);
		this.model.addValue(ERROR_LINE, line);
		this.model.addValue(ERROR_ITEM, comment);
		this.model.addValue(ERROR_MSG, msg.getMessage(mid, prm));
	}

	/**
	 * ファイルインポート
	 * @param br リーダ
	 * @param info メタ情報
	 * @return 処理件数
	 */
	private int insertFile(final Reader br, final Map<String, DBColumnInfo> info) {
		var str = CsvUtil.readLine(br);
		var header = toHeader(info, str);
		if (0 < header.length) {
			str = CsvUtil.readLine(br);
		} else {
			deleteRecords();
			header = getHeader(info);
		}
		header = CamelCase.convert(header);

		final Class<? extends Serializable> cls = this.mm.getDaoClass();
		var count = 0;
		var i = 0;
		while (str != null) {
			i++;
			if (!this.error.contains(String.valueOf(i))
							|| this.ignore.contains(String.valueOf(i))) {
				setValueToModel(header, str);
				if (insertLine(cls)) {
					count++;
				}
			}
			str = CsvUtil.readLine(br);
		}

		setCount(i);

		return count;
	}

	/**
	 * 行インポート
	 * @param cls モデルクラス
	 * @return 処理された場合 true を返す。
	 */
	private boolean insertLine(final Class<? extends Serializable> cls) {
		try (var dao = JdbcSource.getDao(getSchema())) {
			dao.setNoWait(false);
			if (this.model.getString("Id") != null) {
				final var obj = dao.findByIdWithLock(
								cls, NumberUtil.toLong(this.model.getString("Id")));
				if (obj != null) {
					ModelUtil.setModelValue(obj, this.model);
					setUpdateInfo(obj);
					increment(obj);
					dao.update(obj);
					return true;
				}
			}

			final var obj = Factory.create(cls);
			ModelUtil.setModelValue(obj, this.model);
			setUpdateInfo(obj);
			setCreateInfo(obj);
			setVersion(obj, 1);
			dao.insert(obj);
			return true;
		} catch (final DaoConstraintException ex) {
			if (!ex.isNoWait()) {
				throw ex;
			}
			return false;
		}
	}

	/**
	 * レコード削除
	 */
	private void deleteRecords() {
		try (var dao = JdbcSource.getDao(getSchema())) {
			dao.execute("DELETE FROM " + this.mm.getTable());
		}
	}

	/**
	 * 値をモデルに設定
	 * @param header ヘッダ
	 * @param str 値文字列
	 */
	private void setValueToModel(final String[] header, final String str) {
		final var vals = CsvUtil.toArray(str);
		for (var i = 0; i < header.length && i < vals.length; i++) {
			this.model.setValue(header[i], vals[i]);
		}
	}
}
