#!/usr/bin/python3

#	cve-manager : CVE management tool
#	Copyright (C) 2017-2025 Alexey Appolonov
#
#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program.  If not, see <http://www.gnu.org/licenses/>.

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

import argparse
from mysql.connector    import Error as MySQLError
from sys                import argv
from datetime           import datetime, timedelta
from cve_manager.desc   import HISTORY
from cve_manager.common import Init, NewArgParser, DateTimeStr
from cve_manager.conf   import DB_CON_SEC, LOCAL_SYS
from cve_manager.const  import FIX_VULNERABLE_VER
from cve_manager.db     import DB, QueryToFetchIssues, PrefixOfHistoryTables

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Parsing the arguments

argparser = argparse.ArgumentParser(description=HISTORY)
argparser.add_argument(
	'-s', '--store',
	metavar='N_DAYS', type=int,
	help='Number of days "history" tables should be stored '
	'(older "history" tables will be removed)'
	)
argparser = NewArgParser(base=argparser, ptype='m')
args = argparser.parse_args()

if len(argv) < 2:
	argparser.print_help()
	exit(1)

if args.store and args.store < 0:
	args.store = 0

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Initialising a printer and reading the configuration file

printer, conf = Init(args)
mysql_config, = conf.Get([DB_CON_SEC])

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Custom datatypes

class HistoryTable():

	def __init__(self, type_of_history, type_of_issues, branch_name,
			table_name=''):

		self.prefix = \
			PrefixOfHistoryTables(type_of_history, type_of_issues, branch_name)
		self.name = ''
		self.date_time = []

		if table_name:
			if self.prefix and table_name.find(self.prefix) == 0:
				self.name = table_name
		else:
			self.name = self.__NewName()

		if self.name:
			date_time_str = self.name[len(self.prefix):]
			self.date_time = [int(v) for v in date_time_str.split('_')]

	# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

	def __NewName(self):

		if not self.prefix:
			return ''

		t = list(datetime.now().timetuple())[:6]

		return self.prefix + DateTimeStr(t)

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Drop outdated history tables

def DropOutdatedHistoryTables(target, type_of_issues, branch):

	if args.store == None:
		return True

	msg  = 'Dropping outdated tables with '
	msg += f'unfixed {type_of_issues.upper()} issues ' if target == 'issues' \
		else 'maps of package names '
	msg += 'of the local system' if branch == LOCAL_SYS \
		else f'of {branch} branch'
	printer.LineBegin(msg)

	db = DB(printer)
	if not db.Connect(mysql_config):
		return False

	# Getting existing history tables
	history_tables = []
	for table_name in \
			db.ShowHistoryTables(target, type_of_issues, branch):
		history_table = \
			HistoryTable(target, type_of_issues, branch, table_name)
		history_tables.append(history_table)

	# Checking date of the tables and dropping outdated ones
	now = datetime.now()
	delta = timedelta(args.store)
	counter = 0
	for history_table in history_tables:
		if not history_table.name or not history_table.date_time:
			continue
		if now - datetime(*history_table.date_time) > delta:
			if not db.DropTable(history_table.name):
				return False
			counter += 1
			printer.LineAddExtra(history_table.name)

	msg = f'{f"{counter}" if counter > 0 else "No"} outdated ' \
		f'table{"s" if counter != 1 else ""}'
	printer.Success(msg)

	return True

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Save current unfixed issues or current map of package names

def Save(target, type_of_issues, branch):

	msg  = 'Saving '
	msg += f'unfixed {type_of_issues.upper()} issues ' if target == 'issues' \
		else 'a map of package names '
	msg += 'of the local system' if branch == LOCAL_SYS \
		else f'of {branch} branch'
	printer.LineBegin(msg)

	db = DB(printer)
	if not db.Connect(mysql_config):
		return False

	new_history_table = HistoryTable(target, type_of_issues, branch)
	if not new_history_table.name:
		printer.Err('Can\'t generate table name')
		return False

	if target == 'issues':
		issues_table = '_'.join([branch, type_of_issues, 'issues'])
		select_unfixed_issues = ''
		if db.TableExists(issues_table):
			condition = f"{issues_table}.fix = '{FIX_VULNERABLE_VER}'"
			select_unfixed_issues = QueryToFetchIssues(branch, type_of_issues,
				condition, acl=False, fix=False, scores=False, disputed=False)
		query = (
			f'CREATE TABLE {new_history_table.name} ('
			'vul_year YEAR, '
			'vul_num VARCHAR(8), '
			'name VARCHAR(128) COLLATE utf8mb4_0900_as_cs, '
			f'{type_of_issues}_name VARCHAR(128), '
			'ver VARCHAR(64), '
			'rel VARCHAR(64), '
			'vul_ver TEXT, '
			f'CONSTRAINT pk_{new_history_table.name} '
				'PRIMARY KEY (vul_year, vul_num, name)) '
			f'{select_unfixed_issues}'
			)
	else:
		src_table = f'{branch}_src'
		select_names = ''
		if db.TableExists(src_table):
			select_names = f'SELECT name, nvd_name, fstec_name FROM {src_table}'
		query = (
			f'CREATE TABLE {new_history_table.name} ('
			'name VARCHAR(128) COLLATE utf8mb4_0900_as_cs, '
			'nvd_name VARCHAR(128), '
			'fstec_name VARCHAR(128), '
			f'CONSTRAINT pk_{new_history_table.name} '
				'PRIMARY KEY (name)) '
			f'{select_names}'
			)

	if db.Cursor(query) == None:
		return False

	printer.Success()

	return True

# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

if __name__ == '__main__':

	def Execute(params):
		target = params[0]
		if target not in ('issues', 'map'):
			printer.Err(f'Wrong type of history tables "{target}"')
			exit(1)
		try:
			if not DropOutdatedHistoryTables(*params) or not Save(*params):
				exit(1)
		except MySQLError:
			exit(1)

	# Getting a list of available branches and a list of existing types of issues
	available_branches, err = conf.SelectBranches()
	if not err:
		types_of_issues, err = conf.SelectDataSources()
	if err:
		printer.Err(err)

	# For each available branch
	for branch in available_branches:
		# For each type of data sources
		for type_of_issues in types_of_issues:
			Execute(['issues', type_of_issues, branch])
		Execute(['map', '', branch])

	exit(0)
