package ru.bitel.bgbilling.modules.inet.dyn.accounting.detail;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.net.InetAddress;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import ru.bitel.bgbilling.apps.inet.accounting.detail.InetFlowSelector;
import ru.bitel.bgbilling.common.BGException;
import ru.bitel.bgbilling.kernel.network.datalog.IPDataLogUtils;
import ru.bitel.bgbilling.kernel.network.datalog.IPDataLogUtils.FlowDetail;
import ru.bitel.bgbilling.kernel.network.datalog.hourly.IPHourlyDataLog;
import ru.bitel.bgbilling.kernel.network.flow.FlowArray;
import ru.bitel.bgbilling.kernel.network.flow.FlowSelector;
import ru.bitel.bgbilling.server.util.Setup;
import ru.bitel.common.ParameterMap;
import ru.bitel.common.Preferences;
import ru.bitel.common.TimeUtils;
import ru.bitel.common.Utils;

/**
 * Экспорт flow-данных из файлов BGDL в CSV формат.
 * 
 * @author amir
 */
public class FlowExport
{
	public static void main( String[] args )
		throws IOException, BGException
	{
		final Setup setup = Setup.getSetup();
		
		new FlowExport().export( setup, args );
	}

	/**
	 * Экспорт с использованием командной строки.
	 * @param setup setup приложения или null.
	 * @param command строка с параметрами.
	 * @throws IOException
	 * @throws BGException
	 */
	public void export( ParameterMap setup, String command )
		throws IOException, BGException
	{
		final List<String> args = new ArrayList<>();
		// разбор агрументов
		final Matcher m = Pattern.compile( "(?:\\A|[\\s]*)(?:(?:\"(.+?)\")|(?:(.+?)))(?:[\\s]|\\z)" ).matcher( command );

		while( m.find() )
		{
			String k = m.group( 1 );
			if ( k == null )
			{
				k = m.group( 2 );
			}

			args.add( k );
		}

		export( setup, args.toArray( new String[args.size()] ) );
	}

	/**
	 * Экспорт с использованием командной строки.
	 * @param setup setup приложения или null.
	 * @param args параметры
	 * @throws IOException
	 * @throws BGException
	 */
	protected void export( ParameterMap setup, String[] args )
		throws IOException, BGException
	{
		int agentDeviceId = 0;
		Date timeFrom = null;
		Date timeTo = null;
		Set<Integer> ifaces = null;
		byte[] addressFrom = null;
		byte[] addressTo = null;
		String fileName = null;
		String timestampFormat = null;
		String timeZoneName = null;
		int maxSort = 30000;
		String directory = null;
		boolean writeIfaces = true;

		for ( int i = 0, size = args.length - 1; i < size; i++ )
		{
			String k = args[i];
			String v = args[i + 1].trim();

			if ( "-s".equals( k ) )
			{
				agentDeviceId = Utils.parseInt( v );
			}
			else if ( "-h".equals( k ) )
			{
				timeFrom = TimeUtils.parseDate( v, "yyyy-MM-dd-HH" );
				Calendar calendar = new GregorianCalendar();
				calendar.setTime( timeFrom );
				calendar.add( Calendar.HOUR_OF_DAY, 1 );
				calendar.add( Calendar.MILLISECOND, -1 );
				timeTo = calendar.getTime();
			}
			else if ( "-tFrom".equals( k ) )
			{
				timeFrom = TimeUtils.parseDate( v, "yyyy-MM-dd'T'HH:mm:ss" );
			}
			else if ( "-tTo".equals( k ) )
			{
				timeTo = TimeUtils.parseDate( v, "yyyy-MM-dd'T'HH:mm:ss" );
			}
			else if ( "-i".equals( k ) )
			{
				ifaces = Utils.toIntegerSet( v );
			}
			else if ( "-r".equals( k ) )
			{
				String[] vv = v.split( "-" );

				if ( vv.length == 1 )
				{
					addressFrom = addressTo = InetAddress.getByName( v ).getAddress();
				}
				else if ( vv.length == 2 )
				{
					addressFrom = InetAddress.getByName( vv[0] ).getAddress();
					addressTo = InetAddress.getByName( vv[1] ).getAddress();
				}
			}
			else if ( "-f".equals( k ) )
			{
				fileName = v;
			}
			else if ( "-tFmt".equals( k ) )
			{
				timestampFormat = v;
			}
			else if ( "-tZone".equals( k ) )
			{
				timeZoneName = v;
			}
			else if ( "-dir".equals( k ) )
			{
				directory = v;
			}
			else if ( "-maxSort".equals( k ) )
			{
				maxSort = Utils.parseInt( v );
			}
			else if ( "-writeIfaces".equals( k ) )
			{
				writeIfaces = Utils.parseBoolean( v );
			}
		}

		if ( setup == null && Utils.isBlankString( directory ) )
		{
			throw new BGException( "Parameter -dir must not be empty." );
		}

		if ( Utils.isBlankString( fileName ) )
		{
			throw new BGException( "Parameter -f must not be empty." );
		}

		if ( agentDeviceId <= 0 )
		{
			throw new BGException( "Parameter -s must not be empty." );
		}

		if ( timeFrom == null || timeTo == null )
		{
			throw new BGException( "Parameter -h (or -tFrom and -tTo) must not be empty." );
		}

		int timeZoneDelta = 0;
		if ( !Utils.isEmptyString( timeZoneName ) )
		{
			TimeZone processTimeZone = TimeZone.getTimeZone( timeZoneName );
			TimeZone currentTimeZone = TimeZone.getDefault();

			timeZoneDelta = processTimeZone.getRawOffset() - currentTimeZone.getRawOffset();
		}

		export( setup, directory, agentDeviceId, timeFrom, timeTo, ifaces, addressFrom, addressTo, maxSort, writeIfaces, fileName, timestampFormat, timeZoneDelta );
	}

	/**
	 * Экспорт данных flow.
	 * @param setup setup приложения или null.
	 * @param directory расположение директории с flow-логами. Нужно указывать, если setup - null или местоположение отлично от того, что указано в setup.
	 * @param agentDeviceId ID устройства-агента flow-потока.
	 * @param timeFrom начало периода, с которого нужно производить выборку.
	 * @param timeTo конец диапазона, с которого нужно производить выборку.
	 * @param ifaces интерфейсы, по которым будет происходить выборка.
	 * @param addressFrom начало диапазона адресов, по которым будет происходить выборка.
	 * @param addressTo конец диапазона адресов, по которым будет происходить выборка.
	 * @param maxSort кол-во выбранных записей для одного лог-файла, когда сортировку по времени нужно прекратить (для экономии памяти).
	 * @param writeIfaces нужно ли отображать интерфейсы.
	 * @param fileName имя (путь) файла, в который нужно сохранить выгрузку.
	 * @param timestampFormat формат отображения времени или null.
	 * @param timeZoneDelta дельта временной зоны, если указана.
	 * @throws IOException
	 */
	public void export( ParameterMap setup, String directory, int agentDeviceId, Date timeFrom, Date timeTo, Set<Integer> ifaces, byte[] addressFrom,
						byte[] addressTo, int maxSort, boolean writeIfaces, String fileName, String timestampFormat, int timeZoneDelta )
		throws IOException
	{
		FileWriter fileWriter = new FileWriter( fileName );
		BufferedWriter bufferedWriter = new BufferedWriter( fileWriter, 64 * 1024 );

		export( setup, directory, agentDeviceId, timeFrom, timeTo, ifaces, addressFrom, addressTo, maxSort, writeIfaces, bufferedWriter, timestampFormat, timeZoneDelta );

		bufferedWriter.flush();
		fileWriter.flush();

		fileWriter.close();
	}

	/**
	 * Экспорт данных flow.
	 * @param setup setup приложения или null.
	 * @param directory расположение директории с flow-логами. Нужно указывать, если setup - null или местоположение отлично от того, что указано в setup.
	 * @param agentDeviceId ID устройства-агента flow-потока.
	 * @param timeFrom начало периода, с которого нужно производить выборку.
	 * @param timeTo конец диапазона, с которого нужно производить выборку.
	 * @param ifaces интерфейсы, по которым будет происходить выборка.
	 * @param addressFrom начало диапазона адресов, по которым будет происходить выборка.
	 * @param addressTo конец диапазона адресов, по которым будет происходить выборка.
	 * @param maxSort кол-во выбранных записей для одного лог-файла, когда сортировку по времени нужно прекратить (для экономии памяти).
	 * @param writeIfaces нужно ли отображать интерфейсы.
	 * @param writer {@link Writer}
	 * @param timestampFormat формат отображения времени или null.
	 * @param timeZoneDelta дельта временной зоны, если указана.
	 * @throws IOException
	 * @see Writer
	 * @see SimpleDateFormat
	 */
	public void export( ParameterMap setup, String directory, int agentDeviceId, Date timeFrom, Date timeTo, Set<Integer> ifaces, byte[] addressFrom,
						byte[] addressTo, int maxSort, boolean writeIfaces, Writer writer, String timestampFormat, int timeZoneDelta )
		throws IOException
	{
		SortedSet<Calendar> hours = getLogHours( timeFrom, timeTo );

        InetFlowSelector flowSelector = new InetFlowSelector( timeFrom.getTime(), ((timeTo.getTime() / 1000) * 1000) + 999, ifaces, addressFrom, addressTo,
            null, (short)0, null, timeZoneDelta );

		export( setup, "datalog.flow.dir", directory, agentDeviceId, new ArrayList<>( hours ), flowSelector, maxSort, writeIfaces, writer, timestampFormat, timeZoneDelta );
	}

	protected static SortedSet<Calendar> getLogHours( Date timeFrom, Date timeTo )
	{
		SortedSet<Calendar> result = new TreeSet<Calendar>();

		Calendar hour = new GregorianCalendar();
		hour.setTime( timeFrom );
		TimeUtils.clear_MIN_MIL_SEC( hour );

		Calendar end = new GregorianCalendar();
		end.setTime( timeTo );

		while( TimeUtils.hourDelta( hour, end ) >= 0 )
		{
			result.add( hour );

			hour = (Calendar)hour.clone();
			hour.add( Calendar.HOUR_OF_DAY, 1 );
		}

		return result;
	}

	/**
	 * Экспорт данных flow.
	 * @param setup setup приложения или null.
	 * @param directoryKey ключ параметра в setup - расположения директории с flow-логами.
	 * @param directoryValue расположение директории с flow-логами. Нужно указывать, если setup - null или местоположение отлично от того, что указано в setup.
	 * @param agentDeviceId ID устройства-агента flow-потока.
	 * @param hours обрабатываемые часы.
	 * @param flowSelector выборщик записей.
	 * @param maxSort кол-во выбранных записей для одного лог-файла, когда сортировку по времени нужно прекратить (для экономии памяти).
	 * @param writeIfaces нужно ли отображать интерфейсы.
	 * @param writer {@link Writer}
	 * @param timestampFormat формат отображения времени или null.
	 * @param timeZoneDelta дельта временной зоны, если указана.
	 * @throws IOException
	 * @see Writer
	 * @see SimpleDateFormat
	 */
	public void export( ParameterMap setup, final String directoryKey, final String directoryValue, final int agentDeviceId,
						final List<Calendar> hours, final FlowSelector flowSelector, final int maxSort, final boolean writeIfaces,
						final Writer writer, final String timestampFormat, final int timeZoneDelta )
		throws IOException
	{
		if ( Utils.notBlankString( directoryValue ) )
		{
			Preferences prefs = new Preferences();
			prefs.set( directoryKey, directoryValue );
			setup = prefs;
		}

		final Iterable<IPHourlyDataLog> logs = IPDataLogUtils.newIPHourlyDataLogIterable( setup, directoryKey, agentDeviceId, hours );
		final Iterable<FlowArray<FlowDetail>> flowDetailIterable = IPDataLogUtils.newFlowDetailIterable( logs, flowSelector, maxSort );

		writeHeader( writer, writeIfaces );

		final DateFormat dateFormat;
		final Date utilDate = new Date();
		if ( Utils.notBlankString( timestampFormat ) )
		{
			dateFormat = new SimpleDateFormat( timestampFormat );
		}
		else
		{
			dateFormat = null;
		}

		for ( final FlowArray<FlowDetail> f : flowDetailIterable )
		{
			for ( int i = 0, size = f.size; i < size; i++ )
			{
				final FlowDetail flow = f.array[i];

				writeRecord( writer, flow, dateFormat, utilDate, timeZoneDelta, writeIfaces );
			}
		}

		writer.flush();
	}

	/**
	 * Запись заголовка.
	 * @param writer {@link Writer}
	 * @param writeIfaces нужно ли отображать интерфейсы.
	 * @throws IOException
	 */
	protected void writeHeader( final Writer writer, boolean writeIfaces )
		throws IOException
	{
		writer.write( "Timestamp\tFromIp\tFromPort\tToIp\tToPort\tBytes" );

		if ( writeIfaces )
		{
			writer.write( "\tFromIface\tToIface" );
		}

		writer.write( "\tProtocol\n" );
	}

	/**
	 * Запись строки с данными.
	 * @param writer {@link Writer}
	 * @param flow flow-запись
	 * @param dateFormat формат отображения времени, если указан.
	 * @param utilDate
	 * @param timeZoneDelta дельта временной зоны, если указана.
	 * @param writeIfaces нужно ли отображать интерфейсы.
	 * @throws IOException
	 * @see Writer
	 * @see SimpleDateFormat
	 */
	protected void writeRecord( final Writer writer, final FlowDetail flow, final DateFormat dateFormat, final Date utilDate, final int timeZoneDelta, boolean writeIfaces )
		throws IOException
	{
		if ( dateFormat != null )
		{
			utilDate.setTime( flow.getMilliseconds() + timeZoneDelta );
			writer.write( dateFormat.format( utilDate ) );
		}
		else
		{
			writer.write( String.valueOf( flow.getMilliseconds() ) );
		}

		writer.write( '\t' );
		writer.write( String.valueOf( flow.getSrcIpAddress() ) );
		writer.write( '\t' );
		writer.write( String.valueOf( flow.getSrcPort() ) );
		writer.write( '\t' );
		writer.write( String.valueOf( flow.getDstIpAddress() ) );
		writer.write( '\t' );
		writer.write( String.valueOf( flow.getDstPort() ) );
		writer.write( '\t' );
		writer.write( String.valueOf( flow.getOctets() ) );

		if ( writeIfaces )
		{
			writer.write( '\t' );
			writer.write( String.valueOf( flow.getInputInterface() ) );
			writer.write( '\t' );
			writer.write( String.valueOf( flow.getOutputInterface() ) );
		}

		writer.write( '\t' );
		writer.write( String.valueOf( flow.getProtocol() ) );

		writer.write( '\n' );
	}
}
