#!/usr/bin/python3
SVER = '2.0.4'
##############################################################################
# linkchk.py - SCA Pattern Link Verification Tool
# Copyright (C) 2023 SUSE LLC
#
# Description:  Validates META_LINK solution URLs to ensure they are valid.
#               Supports python and perl patterns with *.py and *.pl extensions.
# Modified:     2023 Aug 04
#
##############################################################################
#
#  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; version 2 of the License.
#
#  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/>.
#
#  Authors/Contributors:
#     Jason Record <jason.record@suse.com>
#
##############################################################################

import sys
import os
import re
import getopt
import signal
import configparser
import patdevel as pd
from datetime import timedelta
from timeit import default_timer as timer

##############################################################################
# Global Options
##############################################################################

recurse_directory = False
given_file = ''
pattern_list = []
c_ = {'current': 0, 'pat_total': 0, 'link_total': 0, 'nosolutions': 0, 'badconnection': 0, "badurl": 0, "bugid": 0, "ping": 0, "oldhosts": 0, "active_pattern": '', "active_link": ''}
invalid_links = {}
elapsed = -1
start = 0
end = 0
title_string = "SCA Pattern Link Verification Tool"

##############################################################################
# Functions
##############################################################################

def usage():
	"Displays usage information"
	display = "  {:33s} {}"
	print("Usage: linkchk [options] [filepath]")
	print()
	print("Options:")
	print(display.format("-h, --help", "Display this help"))
	print(display.format("-r, --recurse", "Validate patterns and archives recursively found in the directory structures"))
	print(display.format('-l <level>, --log_level <level>', "Set log level, default: Minimal"))
	print(display.format('', "0 Quiet, 1 Minimal, 2 Normal, 3 Verbose, 4 Debug"))
	print()

def signal_handler(sig, frame):
	print("\n\nAborting...\n")
	show_summary()
	sys.exit(1)

def show_summary():
	display = "{0:26} {1}"
	print("Summary")
	print("----------------------------------")
	print(display.format("Elsapsed Runtime", str(elapsed).split('.')[0]))
	print(display.format("Total Patterns Checked", c_['pat_total']))
	print(display.format("Total Links Evaluated", c_['link_total']))
	print(display.format("Patterns without Solutions", c_['nosolutions']))
	print(display.format("Invalid Connection Links", c_['badconnection']))
	print(display.format("Invalid URLs", c_['badurl']))
	print(display.format("Servers Down", c_['ping']))
	print(display.format("Invalid Bug Content", c_['bugid']))
	print(display.format("Old Host Domains", c_['oldhosts']))
	print(display.format("Active Pattern", c_['active_pattern']))
	print(display.format("Active Link", c_['active_link']))
	print()
	print("Invalid Links")
	print("----------------------------------")
	ldisplay = "  {0:25} {1}"
	if len(invalid_links) > 0:
		for pattern in invalid_links.keys():
			print(pattern)
			if invalid_links[pattern]['nosolutions']:
				print(ldisplay.format('! No Solutions', pattern))
			del invalid_links[pattern]['nosolutions']
			for key, value in invalid_links[pattern].items():
				print(ldisplay.format(value, key))
	else:
		print("None")
	print()

##############################################################################
# Main
##############################################################################

def main(argv):
	"main entry point"
	global SVER, pattern_list, recurse_directory, c_, invalid_links, elapsed, start, end
	start = timer()
	
	if( os.path.exists(pd.config_file) ):
		config.read(pd.config_file)
		pat_dir = pd.config_entry(config.get("Security", "pat_dir"), '/')
		config_log_level = pd.config_entry(config.get("Common", "log_level"))
		config_logging = msg.validate_level(config_log_level)
		if( config_logging >= msg.LOG_QUIET ):
			msg.set_level(config_logging)
		else:
			print("Warning: Invalid log level in config file, using instance default")

	try:
		(optlist, args) = getopt.gnu_getopt(argv[1:], "hrl:", ["help", "recurse", "log_level="])
	except getopt.GetoptError as exc:
		pd.title(title_string, SVER)
		print("Error:", exc, file=sys.stderr)
		sys.exit(2)
	for opt, arg in optlist:
		if opt in {"-h", "--help"}:
			pd.title(title_string, SVER)
			usage()
			sys.exit(0)
		elif opt in {"-r", "--recurse"}:
			recurse_directory = True
		elif opt in {"-l", "--log_level"}:
			user_logging = msg.validate_level(arg)
			if( user_logging >= msg.LOG_QUIET ):
				msg.set_level(user_logging)
			else:
				print("Warning: Invalid log level, using instance default")

	if( msg.get_level() > msg.LOG_QUIET ):
		pd.title(title_string, SVER)

	msg.normal("Log Level", msg.get_level_str())

	given_file = ''
	if len(args) > 0:
		# TODO: support multiple files given on the command line to validate
		given_file = args[0]
		if os.path.isdir(given_file):
			path = os.path.abspath(given_file)
			pattern_list = pd.get_pattern_list(path, recurse_directory)
			if recurse_directory:
				msg.min("Recursively processing directory", path)
			else:
				msg.min("Processing directory", path)
		elif os.path.isfile(given_file):
			given_file = os.path.abspath(given_file)
			msg.min("Processing file", given_file)
			pattern_list.append(given_file)
		else:
			print("Error: Invalid file or path - " + given_file)
			sys.exit(5)
	else:
		path = pat_dir
		if recurse_directory:
			msg.min("Recursively processing directory", path)
		else:
			msg.min("Processing directory", path)
		pattern_list = pd.get_pattern_list(path, recurse_directory)

	c_['pat_total'] = len(pattern_list)
	vdisplay = "  {0:23} {1}"
	csize = len(str(c_['pat_total']))
	cdisplay = "{0:0" + str(csize) + "d}/{1} {2}"
	if( msg.get_level() == msg.LOG_MIN ):
		bar = pd.ProgressBar("Validating links: ", c_['pat_total'])

	for pattern in pattern_list:
		c_['current'] += 1
		if( msg.get_level() == msg.LOG_MIN ):
			bar.inc_count()
		msg.normal(cdisplay.format(c_['current'], c_['pat_total'], pattern))
		c_['active_pattern'] = pattern
		bad_urls = []
		url_list = pd.get_links_from_pattern_file(pattern)
		if( len(url_list) > 0 ):
			bad_urls, c_ = pd.validate_link_list(url_list, c_, msg)
			if( len(bad_urls) > 0):
				invalid_links[pattern] = bad_urls
				if( len(bad_urls) == len(url_list) ):
					invalid_links[pattern]['nosolutions'] = True
					c_['nosolutions'] += 1
					status = "! No Solutions"
					msg.normal(vdisplay.format(status, pattern))
				else:
					invalid_links[pattern]['nosolutions'] = False
		else:
			invalid_links[pattern] = {}
			invalid_links[pattern]['nosolutions'] = True
			c_['nosolutions'] += 1
			status = "! No Solution Links"
			msg.normal(vdisplay.format(status, pattern))
		msg.debug(invalid_links)

		if( msg.get_level() == msg.LOG_MIN ):
			bar.update()
	if( msg.get_level() == msg.LOG_MIN ):
		bar.finish()
	c_['active_pattern'] = "None" # Shows the active pattern file if linkchk is aborted
	c_['active_link'] = "None" # Shows the active link being checked if linkchk is aborted
	end = timer()
	elapsed = str(timedelta(seconds=end-start))
	msg.normal()
	show_summary()
		
# Entry point
if __name__ == "__main__":
	signal.signal(signal.SIGINT, signal_handler)
	config = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
	msg = pd.DisplayMessages()
	main(sys.argv)

