#!/usr/bin/env python3
"""
Check internal links in md file in or under hugo directory.
Used by BornAgain CI to ensure that all internal links work in the online documentation.

Copyright Forschungszentrum Julich GmbH 2025.
License:  Public Domain
"""
import glob, os, re, sys

# Shortcode file path mappings
# Maps (shortcode_name, parameter_name) to path transformation function
# Paths starting with / are treated as absolute from hugo/content
SHORTCODE_MAPPINGS = {
    ('figscg', 'src'): lambda p: p,  # Direct path, already starts with /
    ('show-ex', 'file'): lambda p: '/static/py/auto/Examples/' + p,
    ('ref-ex', None): lambda p: '/auto/Examples/' + p,  # Absolute from repo root
    ('ref-data-scat2d', None): lambda p: '/testdata/scatter2d/' + p.strip(),
    ('ref-data-spec', None): lambda p: '/testdata/specular/' + p.strip(),
    ('ref-src', None): lambda p: '/' + p,  # Absolute from repo root
    ('ref-class', None): lambda parts: '/' + parts[0] + '/' + parts[1] + '.h',  # Two args
}


def print_broken_links(first_line_printed, broken_links_count, lin,
                       links_list, src_list):
    '''
    Increase the broken links counter by 1 and return it in the output.
    Print the broken links with the corresponding md files in relative form.
    Change first_line_printed in case it was False to indicate that the header
    is printed and return its value.
    '''
    if not first_line_printed:
        print("Broken link: ")
        first_line_printed = True
    broken_links_count += 1
    print('\n', lin)
    print("In: ")
    for i in range(len(links_list)):
        if links_list[i] == lin:
            print(src_list[i])
    return (first_line_printed, broken_links_count)


def make_path_absolute(current_path, relative_path):
    '''
    This is to transform relative path into an absolute one
    example: current_path is ./ref/sim/class, relative_path: ../../file
    as long as there is ../ in the relative path, it delete them, but
    at the same time it delete one level from the current path
    inside while, in the first iteration: relative path becomes ../file
    and current_path becomes ./ref/sim
    '''
    while relative_path[0:3] == '../':
        relative_path = relative_path[3:len(relative_path)]
        current_path = current_path[0:int(current_path.rfind('/'))]
    return f'{current_path}/{relative_path}'


def check_one_file(mdf):
    '''
    Read a file (mdf).
    Check the searched pattern in a file and send each to test.
    '''
    with open(mdf, 'r', encoding="utf8") as f:
        try:
            ftxt = f.read()
        except Exception as e:
            print('A problem appeard while trying to read the file: ' +
                  mdf)
            print(e)
            exit(1)
        # Check regular markdown links
        for caught in re.findall(r'\[.*?\]\((.*?)\)', ftxt):
            check_one_link(mdf, caught)

        # Check shortcode links using the mapping
        # Named parameters with {{< >}} syntax
        for caught in re.findall(r'{{<\s*figscg\s+src="([^"]+)"', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('figscg', 'src')](caught))
        for caught in re.findall(r'{{<\s*show-ex\s+file="([^"]+)"', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('show-ex', 'file')](caught))

        # Positional parameters with {{% %}} syntax
        for caught in re.findall(r'{{% ref-ex "([^"]+)" %}}', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('ref-ex', None)](caught))
        for caught in re.findall(r'{{% ref-data-scat2d ([^ %}]+) %}}', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('ref-data-scat2d', None)](caught))
        for caught in re.findall(r'{{% ref-data-spec ([^ %}]+) %}}', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('ref-data-spec', None)](caught))
        for caught in re.findall(r'{{% ref-src "([^"]+)" %}}', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('ref-src', None)](caught))
        for caught in re.findall(r'{{% ref-class "([^"]+)" "([^"]+)" %}}', ftxt):
            check_one_link(mdf, SHORTCODE_MAPPINGS[('ref-class', None)](caught))

def check_one_link(mdf, caught):
    '''
    Check a one detected pattern (caught) in a file (mdf)
    Checking the format of caught, if it is a string, if it contains special characters,..
    Transform conformal link to a unified form
    Append mdf and link to src_list and to Links_list.
    '''
    try:
        line = str(caught)
    except Exeption as e:
        print('The following error appeard when dealing with file: ')
        print(mdf)
        print('It happened when reading the link as a string')
        print(e)
        exit(1)
    # Strip title attribute if present (e.g., "path/file.png "title text"")
    if ' "' in line:
        line = line[:line.index(' "')]
    # Skip empty strings
    if not line:
        return
    if '.md' in line:
        dotmdlst.append(line)
    if line[0:1] == '/':
        tline = '.' + line
        src_list.append(mdf)
        links_list.append(tline)
    elif line[0:4] == 'http':
        pass  # don't check external links for the time being
    elif line[0] == '#':
        pass  # link target is inside present page - no need for checking
    elif line[0:2] == '{{':
        pass  # TODO, shortcodes to be treated later
    elif line[0:7] == 'mailto:':
        pass
    else:
        pp = mdf[0:int(mdf.rfind('/'))]
        tline = make_path_absolute(pp, line)
        src_list.append(mdf)
        links_list.append(tline)


def check_existence(lin, broken_links_count):
    '''
    Check if the link is broken, print related information if it is the case
    increase broken links count when neccessay
    '''
    first_line_printed = False
    if '#' in lin:
        lin = lin[0:int(lin.rfind('#'))]
    if '.md' in lin:
        dotmdlst.append(lin)
    # Check if this is a static file link (starts with ./img/, ./css/, ./static/, etc.)
    # Static files are in hugo/static/, not hugo/content/
    # Repository files (auto/, testdata/, Sample/, etc.) are in repo root ../
    check_path = lin
    if lin.startswith('./img/') or lin.startswith('./css/') or lin.startswith('./js/') or lin.startswith('./files/'):
        # Convert ./img/... to ../static/img/...
        check_path = '../static/' + lin[2:]
    elif lin.startswith('./static/'):
        # Convert ./static/... to ../static/...
        check_path = '..' + lin[1:]
    elif (lin.startswith('./auto/') or lin.startswith('./testdata/') or lin.startswith('./Sample/') or
          lin.startswith('./scripts/') or lin.startswith('./devtools/') or lin.startswith('./Device/') or
          lin.startswith('./3rdparty/') or lin == './testdata' or lin == './3rdparty'):
        # Convert ./auto/... to ../../auto/... (repository root files, two levels up from hugo/content)
        check_path = '../..' + lin[1:]
    # Check if path exists as file or directory
    if os.path.isdir(check_path):
        # Path is a valid directory, no need to check further
        return broken_links_count
    if not os.path.exists(check_path):
        '''
        os.path.exists() does not give a correct answer when checking for a file
        without its extension.
        example when checking ./ref/example where example is an md file, it returns False
        The goal of the following is first separate the path from the name of the file
        pthchk (path check) in the example is to check ./ref
        fdirchk is the name example.
        Then we have to list the name of what pthchk contains without extensions
        if fdirchk match one of them then the link is not broken
        '''
        pthchk = check_path[0:int(check_path.rfind('/'))]
        fdirchk = check_path[int(check_path.rfind('/')) + 1:len(check_path)]
        direx = os.path.exists(pthchk)
        if not direx:
            (first_line_printed, broken_links_count) = print_broken_links(
                first_line_printed, broken_links_count, lin, links_list,
                src_list)
        elif direx:
            lsta = glob.glob(pthchk + '/*') # list pthchk directory items
            newl = [] # a list to append the same items but without extensions
            for elea in lsta:
                elea = elea[int(elea.rfind('/')) + 1:len(elea)] # here just to get the names without any path part
                if '.' in elea: # if it contains extension
                    elea = elea[0:int(elea.rfind('.'))]
                newl.append(elea.lower())
            if fdirchk not in newl:
                (first_line_printed,
                 broken_links_count) = print_broken_links(
                     first_line_printed, broken_links_count, lin,
                     links_list, src_list)
    return broken_links_count

src_list = []
links_list = []
dotmdlst = []

def check_hugo_links():
    '''
    The main function. it gather first all links in the absolute form,
    then it checks them
    '''
    mdlist = []    
    mdlist = glob.glob('./**/*.md', recursive=True)
    for ele in range(len(mdlist)):
        mdf = str(mdlist[ele])
        check_one_file(mdf)
    if links_list == []:
        print("Links list is empty")
        exit(1)
    disabs = list(set(links_list))
    total_links_count = len(disabs)
    broken_links_count = 0
    for cc in range(len(disabs)):
        link = disabs[cc]
        broken_links_count = check_existence(link, broken_links_count)
    print('--------------------------------------------------')
    print('links containing .md extension are: ', len(dotmdlst))
    print(
        f'{broken_links_count}/{total_links_count} links are broken in total!'
    )
    if broken_links_count > 0:
        exit(1)


if __name__ == '__main__':
    check_hugo_links()
