import os
import re
import json
import urllib.parse

data_root = "/run/media/kevin/web/icpc/data"
ignored_dirs = ["archive.algo.is", "test", "random", "borg3"]

contest_names = {
    "acpc": "Arab Collegiate Programming Contest",
    "bapc": "Benelux Algorithm Programming Championship",
    "bapc-prelim": "Benelux Algorithm Programming Championship Preliminaries (BAPC Prelims)",
    "cerc": "Central Europe Regional Contest",
    "dq": "Duke Qualification Contest",
    "ecna": "East Central North America Regional Contest",
    "finals": "ICPC World Finals",
    "euc": "ICPC Europe Championship",
    "gcpc": "German Collegiate Programming Championship",
    "gnyr": "Greater New York Regional Contest",
    "mcpc": "Mid-Central USA Regional Contest",
    "midatl": "North American Mid-Atlantic Region",
    "nane": "Northeast North America Regional Contest",
    "nac": "North America Championship",
    "nadc": "North America Division Championship",
    "naeast": "North America East Regional Contest (NA East)",
    "naipc": "North American Invitational Programming Contest",
    "naq": "North America Qualifier",
    "nasouth": "North American Big-South super region",
    "ncna": "North Central North America Regional Contest",
    "ncpc": "Nordic Collegiate Programming Contest",
    "neerc": "Northern Eurasia Regional Contest",
    "nwerc": "Northwestern Europe Regional Contest",
    "pacnw": "Pacific Northwest Regional Contest",
    "rmrc": "Rocky Mountain Regional Contest",
    "scusa": "South Central USA Regional Contest",
    "seerc": "Southeastern Europe Regional Contest",
    "ser": "Southeast USA Regional Contest",
    "socal": "Southern California Regional Contest",
    "soeul": "Seoul Regional Contest",
    "swerc": "Southwestern Europe Regional Contest",
    "ukiepc": "United Kingdom & Ireland Evening Programming Contest",
    "waterloo": "Waterloo Local Contests",
    
    # New European subregionals
    "amppz": "Polish Collegiate Programming Contest (AMPPZ)",
    "ctuopen": "Czech Technical University Open (CTU Open)",
    "hcpc": "Hungarian Collegiate Programming Contest (HCPC)",
    "adabyron": "Spanish AdaByron Collegiate Programming Contest",
    "itacpc": "Italian Collegiate Programming Contest (ITACPC)",
    "miup": "Portuguese Inter-University Programming Marathon (MIUP)",
    "grcpc": "Greek Collegiate Programming Contest (GRCPC)",
    "ukr": "Ukrainian Collegiate Programming Contest (UKR)",
    "trcpc": "Turkish Collegiate Programming Contest (TRCPC)"
}

contest_languages = {
    "acpc": "English",
    "bapc": "English",
    "bapc-prelim": "English",
    "cerc": "English",
    "dq": "English",
    "ecna": "English",
    "finals": "English",
    "euc": "English",
    "gcpc": "English",
    "gnyr": "English",
    "mcpc": "English",
    "midatl": "English",
    "nane": "English",
    "nac": "English",
    "nadc": "English",
    "naeast": "English",
    "naipc": "English",
    "naq": "English",
    "nasouth": "English",
    "ncna": "English",
    "ncpc": "English",
    "neerc": "English / Russian",
    "nwerc": "English",
    "pacnw": "English",
    "rmrc": "English",
    "scusa": "English",
    "seerc": "English",
    "ser": "English",
    "socal": "English",
    "soeul": "English / Korean",
    "swerc": "English",
    "ukiepc": "English",
    "waterloo": "English",
    
    # New European subregionals
    "amppz": "Polish",
    "ctuopen": "English",
    "hcpc": "Hungarian",
    "adabyron": "Spanish",
    "itacpc": "English",
    "miup": "English",
    "grcpc": "English",
    "ukr": "Ukrainian",
    "trcpc": "Turkish"
}

# Explicit defined start years for contests that might be empty or new, ensuring they get generated up to 2026!
contest_start_years = {
    "bapc-prelim": 2016,
    "euc": 2024,
    "nane": 2019,
    "naeast": 2023,
    "ctuopen": 2000,
    "amppz": 2005,
    "itacpc": 2023,
    "miup": 2015,
    "grcpc": 2023,
    "ukr": 2010,
    "trcpc": 2015,
    "adabyron": 2015,
    "hcpc": 2015
}

# Contest level general links
contest_general_links = {
    "euc": ["https://euc.icpc.global/"],
    "gcpc": ["https://gcpc.nwerc.eu/materials"],
    "ncpc": ["https://nordic.icpc.io/archive"],
    "nwerc": ["https://www.nwerc.eu/", "https://people.bath.ac.uk/masjhd/NWERC/contest/index.html"],
    "swerc": ["https://swerc.eu/", "https://swerc.eu/2023/past-editions/", "https://swerc.eu/2017/theme/problems/"],
    "seerc": ["http://acm.ro/"],
    "cerc": ["https://cerc.acm.si/", "https://cerc2023.acm.si/", "https://cerc2022.acm.si/#home", "https://contest.felk.cvut.cz/20cerc", "https://cerc.hsin.hr/2016/"],
    "bapc-prelim": ["https://2024.bapc.eu/"],
    "bapc": ["https://2024.bapc.eu/"],
    "ukiepc": ["http://ukiepc.info/", "https://archive.algo.is/icpc/nwerc/ukiepc"],
    "finals": ["https://icpc.global/worldfinals/past-problems"],
    "naq": ["http://serjudging.vanb.org/", "https://cs.baylor.edu/~hamerly/icpc/"],
    "nac": ["http://serjudging.vanb.org/"],
    "naipc": ["http://serjudging.vanb.org/"],
    "nadc": ["http://serjudging.vanb.org/"],
    "ser": ["http://serjudging.vanb.org/"],
    "nasouth": ["http://serjudging.vanb.org/"],
    "naeast": ["https://east.na.icpc.global/", "https://east.na.icpc.global/problems/2025"],
    "nane": ["https://ne.na.icpc.global/", "https://na-northeast.icpc.io/"],
    "gnyr": ["http://acmgnyr.org/"],
    "ecna": ["https://na.icpc.global/archive/", "https://github.com/icpc/na-ecna-archive", "https://na.icpc.global/2022-23/regionals/ecna", "https://icpc-ecna.ysu.edu/"],
    "mcpc": ["https://mc.na.icpc.global/archive-1/", "https://web.archive.org/web/20210128093725/https://icpc-midcentral.us/archive/index.html"],
    "scusa": ["http://scusa.lsu.edu/", "https://ld2018.scusa.lsu.edu/problems-regional/", "https://acm2008.cct.lsu.edu/results/problems/"],
    "rmrc": ["https://rocky.icpc.io/", "https://org.coloradomesa.edu/~wmacevoy/rmrc/2013/index.html", "https://github.com/icpc"],
    "pacnw": ["https://pnw.na.icpc.global/", "http://acmicpc-pacnw.org/", "http://acmicpc-pacnw.org/results.htm"],
    "socal": ["https://scl.na.icpc.global/", "http://socalcontest.org/current/index.shtml"],
    "ncna": ["https://na.icpc.global/regionals/ncna-home/ncna-archive/", "http://ncna-region.unl.edu/"]
}

# Year-specific explicit custom Kattis URLs
contest_kattis_custom = {
    "naq": {
        "2014": "https://naq14.kattis.com/contests/s9e496d",
        "2015": "https://naq15.kattis.com/contests/qztosc",
        "2016": "https://naq16.kattis.com/contests/naq16",
        "2017": "https://naq17.kattis.com/contests/naq17",
        "2018": "https://naq18.kattis.com/contests/naq18",
        "2019": "https://naq19.kattis.com/contests/naq19",
        "2020": "https://naq20.kattis.com/contests/naq20",
        "2021": "https://naq21.kattis.com/contests/naq21",
        "2022": "https://naq22.kattis.com/contests",
        "2023": "https://naq23.kattis.com/contests",
        "2024": "https://naq24.kattis.com/contests"
    },
    "nac": {
        "2020": "https://nac20.kattis.com/",
        "2021": "https://nac21.kattis.com/",
        "2022": "https://nac22.kattis.com/",
        "2023": "https://nac23.kattis.com/",
        "2024": "https://nac24.kattis.com/"
    },
    "naipc": {
        "2012": "https://naipc12.kattis.com/",
        "2013": "https://naipc13.kattis.com/",
        "2014": "https://naipc14.kattis.com/",
        "2015": "https://naipc15.kattis.com/",
        "2016": "https://naipc16.kattis.com/",
        "2017": "https://naipc17.kattis.com/",
        "2018": "https://naipc18.kattis.com/",
        "2019": "https://naipc19.kattis.com/"
    },
    "midatl": {
        "2017": "https://mausa17.kattis.com/contests/mausa17/standings",
        "2018": "https://mausa18.kattis.com/contests/mausa18/standings",
        "2019": "https://mausa19.kattis.com/contests/mausa19/standings",
        "2020": "https://mausa20.kattis.com/contests/mausa20/standings",
        "2021": "https://mausa21.kattis.com/contests/mausa21/standings",
        "2022": "https://nasouth22.kattis.com/contests",
        "2023": "https://nasouth23.kattis.com/contests/nasouth23d1"
    },
    "nadc": {
        "2021": "https://nadc21.kattis.com/"
    },
    "ser": {
        "2022": "https://nasouth22.kattis.com/contests",
        "2023": "https://nasouth23.kattis.com/contests/nasouth23d1"
    },
    "naeast": {
        "2023": "https://naeast23.kattis.com/contests/naeast23/standings",
        "2024": "https://naeast24.kattis.com/",
        "2025": "https://naeast25.kattis.com/contests"
    },
    "nane": {
        "2019": "https://nena19.kattis.com/contests/nena19",
        "2020": "https://nena20.kattis.com/contests/nena20",
        "2021": "https://nena21.kattis.com/contests/nena21",
        "2022": "https://nena22.kattis.com/contests",
        "2023": "https://naeast23.kattis.com/contests/naeast23/standings",
        "2024": "https://naeast24.kattis.com/",
        "2025": "https://naeast25.kattis.com/contests"
    },
    "gnyr": {
        "2024": "https://naeast24.kattis.com/",
        "2025": "https://naeast25.kattis.com/contests"
    },
    "ecna": {
        "2015": "https://ecna15.kattis.com/contests/ecna15",
        "2016": "https://ecna16.kattis.com/contests/ecna16",
        "2017": "https://ecna17.kattis.com/contests/ecna17",
        "2018": "https://ecna18.kattis.com/contests/ecna18",
        "2019": "https://ecna19.kattis.com/contests/ecna19",
        "2020": "https://ecna20.kattis.com/contests/ecna20",
        "2021": "https://ecna21.kattis.com/contests/ecna21",
        "2022": "https://ecna22.kattis.com/contests",
        "2023": "https://naeast23.kattis.com/contests/naeast23/standings",
        "2024": "https://naeast24.kattis.com/",
        "2025": "https://naeast25.kattis.com/contests"
    },
    "mcpc": {
        "2015": "https://mcpc15.kattis.com/contests/en2mpy",
        "2016": "https://mcpc16.kattis.com/contests/mcpc16",
        "2017": "https://mcpc17.kattis.com/contests/mcpc17",
        "2018": "https://mcpc18.kattis.com/contests/mcpc18",
        "2019": "https://mcpc19.kattis.com/contests/mcpc19",
        "2020": "https://mcpc20.kattis.com/contests/mcpc20",
        "2021": "https://mcpc21.kattis.com/contests/mcpc21",
        "2022": "https://mcpc22.kattis.com/contests",
        "2023": "https://mcpc23.kattis.com/contests",
        "2024": "https://mcpc24.kattis.com/contests",
        "2025": "https://mcpc25.kattis.com/contests"
    },
    "scusa": {
        "2018": "https://scusa18.kattis.com/contests/scusa18",
        "2019": "https://scusa19.kattis.com/contests/scusa19",
        "2020": "https://scusa20.kattis.com/contests/scusa20",
        "2022": "https://nasouth22.kattis.com/contests",
        "2023": "https://nasouth23.kattis.com/contests/nasouth23d1"
    },
    "rmrc": {
        "2023": "https://rmc23.kattis.com/"
    },
    "ncna": {
        "2017": "https://ncna17.kattis.com/contests/ncna17",
        "2018": "https://ncna18.kattis.com/contests/ncna18",
        "2019": "https://ncna19.kattis.com/contests/ncna19",
        "2020": "https://ncna20.kattis.com/contests/ncna20",
        "2021": "https://ncna21.kattis.com/contests/ncna21",
        "2022": "https://ncna22.kattis.com/contests",
        "2023": "https://ncna23.kattis.com/contests",
        "2024": "https://ncna24.kattis.com/contests",
        "2025": "https://ncna25.kattis.com/contests"
    }
}

# Live Scraped problem-sources mapping (No fictitious links, 100% matched to crawled data)
kattis_sources_crawled = {
    "bapc": {
        "2012": "Benelux Algorithm Programming Contest (BAPC) 2012",
        "2013": "Benelux Algorithm Programming Contest (BAPC) 2013",
        "2014": "Benelux Algorithm Programming Contest (BAPC) 2014",
        "2015": "Benelux Algorithm Programming Contest (BAPC) 2015",
        "2016": "Benelux Algorithm Programming Contest (BAPC) 2016",
        "2017": "Benelux Algorithm Programming Contest (BAPC) 2017",
        "2018": "Benelux Algorithm Programming Contest (BAPC) 2018",
        "2019": "Benelux Algorithm Programming Contest (BAPC) 2019",
        "2020": "Benelux Algorithm Programming Contest (BAPC) 2020",
        "2021": "Benelux Algorithm Programming Contest (BAPC) 2021",
        "2022": "Benelux Algorithm Programming Contest (BAPC) 2022",
        "2023": "Benelux Algorithm Programming Contest (BAPC) 2023",
        "2024": "Benelux Algorithm Programming Contest (BAPC) 2024",
        "2025": "Benelux Algorithm Programming Contest (BAPC) 2025",
    },
    "bapc-prelim": {
        "2016": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2016",
        "2017": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2017",
        "2018": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2018",
        "2019": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2019",
        "2020": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2020",
        "2021": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2021",
        "2022": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2022",
        "2023": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2023",
        "2024": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2024",
        "2025": "Benelux Algorithm Programming Contest (BAPC) preliminaries 2025",
    },
    "ncpc": {
        "2005": "Nordic Collegiate Programming Contest (NCPC) 2005",
        "2006": "Nordic Collegiate Programming Contest (NCPC) 2006",
        "2007": "Nordic Collegiate Programming Contest (NCPC) 2007",
        "2008": "Nordic Collegiate Programming Contest (NCPC) 2008",
        "2009": "Nordic Collegiate Programming Contest (NCPC) 2009",
        "2010": "Nordic Collegiate Programming Contest (NCPC) 2010",
        "2011": "Nordic Collegiate Programming Contest (NCPC) 2011",
        "2012": "Nordic Collegiate Programming Contest (NCPC) 2012",
        "2013": "Nordic Collegiate Programming Contest (NCPC) 2013",
        "2014": "Nordic Collegiate Programming Contest (NCPC) 2014",
        "2015": "Nordic Collegiate Programming Contest (NCPC) 2015",
        "2016": "Nordic Collegiate Programming Contest (NCPC) 2016",
        "2017": "Nordic Collegiate Programming Contest (NCPC) 2017",
        "2018": "Nordic Collegiate Programming Contest (NCPC) 2018",
        "2019": "Nordic Collegiate Programming Contest (NCPC) 2019",
        "2020": "Nordic Collegiate Programming Contest (NCPC) 2020",
        "2021": "Nordic Collegiate Programming Contest (NCPC) 2021",
        "2022": "Nordic Collegiate Programming Contest (NCPC) 2022",
        "2023": "Nordic Collegiate Programming Contest (NCPC) 2023",
        "2024": "Nordic Collegiate Programming Contest (NCPC) 2024",
        "2025": "Nordic Collegiate Programming Contest (NCPC) 2025",
        "2026": "Nordic Collegiate Programming Contest (NCPC) 2026",
    },
    "gcpc": {
        "2011": "German Collegiate Programming Contest (GCPC) 2011",
        "2012": "German Collegiate Programming Contest (GCPC) 2012",
        "2013": "German Collegiate Programming Contest (GCPC) 2013",
        "2014": "German Collegiate Programming Contest (GCPC) 2014",
        "2015": "German Collegiate Programming Contest (GCPC) 2015",
        "2016": "German Collegiate Programming Contest (GCPC) 2016",
        "2017": "German Collegiate Programming Contest (GCPC) 2017",
        "2018": "German Collegiate Programming Contest (GCPC) 2018",
        "2019": "German Collegiate Programming Contest (GCPC) 2019",
        "2020": "German Collegiate Programming Contest (GCPC) 2020",
        "2021": "German Collegiate Programming Contest (GCPC) 2021",
        "2022": "German Collegiate Programming Contest (GCPC) 2022",
        "2023": "German Collegiate Programming Contest (GCPC) 2023",
        "2024": "German Collegiate Programming Contest (GCPC) 2024",
        "2025": "German Collegiate Programming Contest (GCPC) 2025",
        "2026": "German Collegiate Programming Contest (GCPC) 2026",
    },
    "nwerc": {
        "2003": "Northwestern Europe Programming Contest (NWERC) 2003",
        "2004": "Northwestern Europe Programming Contest (NWERC) 2004",
        "2005": "Northwestern Europe Programming Contest (NWERC) 2005",
        "2006": "Northwestern Europe Programming Contest (NWERC) 2006",
        "2007": "Northwestern European Programming Contest (NWERC) 2007",
        "2008": "Northwestern Europe Contest (NWERC) 2008",
        "2009": "Northwestern Europe Contest (NWERC) 2009",
        "2010": "Northwestern Europe Regional Contest (NWERC) 2010",
        "2011": "Northwestern Europe Regional Contest (NWERC) 2011",
        "2014": "Northwestern Europe Regional Contest (NWERC) 2014",
        "2015": "Northwestern Europe Regional Contest (NWERC) 2015",
        "2016": "Northwestern Europe Regional Contest (NWERC) 2016",
        "2017": "Northwestern Europe Regional Contest (NWERC) 2017",
        "2018": "Northwestern Europe Regional Contest (NWERC) 2018",
        "2019": "Northwestern Europe Regional Contest (NWERC) 2019",
        "2020": "Northwestern Europe Regional Contest (NWERC) 2020",
        "2021": "Northwestern Europe Regional Contest (NWERC) 2021",
        "2022": "Northwestern Europe Regional Contest (NWERC) 2022",
        "2023": "Northwestern Europe Regional Contest (NWERC) 2023",
        "2024": "Northwestern Europe Regional Contest (NWERC) 2024",
        "2025": "Northwestern Europe Regional Contest (NWERC) 2025",
    },
    "naq": {
        "2013": "2013 ACM-ICPC North American Qualifier",
        "2014": "2014 ICPC North America Qualifier",
        "2015": "2015 ICPC North American Qualifier Contest",
        "2016": "2016 ICPC North American Qualifier Contest",
        "2017": "2017 ICPC North American Qualifier Contest",
        "2018": "2018 ICPC North American Qualifier Contest",
        "2019": "2019 ICPC North American Qualifier Contest",
        "2020": "2020 ICPC North American Qualifier Contest",
        "2021": "2021 ICPC North American Qualifier Contest (Jan 2022)",
        "2022": "2022 ICPC North America Qualifier",
        "2023": "2023 ICPC North America Qualifier 30 September",
        "2024": "2024 ICPC North America Qualifier",
        "2025": "2025 ICPC North America Qualifier",
    },
    "nac": {
        "2020": "North America Championship 2020",
        "2021": "ICPC North America Championship 2021",
        "2022": "2022 ICPC North America Championship",
        "2023": "2023 ICPC North America Championship",
        "2024": "2024 ICPC North America Championship",
        "2025": "2025 ICPC North America Championship",
        "2026": "2026 ICPC North America Championship",
    },
    "naipc": {
        "2012": "2012 University of Chicago Invitational Programming Contest",
        "2013": "2013 University of Chicago Invitational Programming Contest",
        "2014": "North American Invitational Programming Contest 2014",
        "2015": "North American Invitational Programming Contest 2015",
        "2016": "North American Invitational Programming Contest 2016",
        "2017": "North American Invitational Programming Contest 2017",
        "2018": "North American Invitational Programming Contest 2018",
        "2019": "North American Invitational Programming Contest 2019"
    },
    "rmrc": {
        "2013": "Rocky Mountain Regional Contest (RMRC) 2013",
        "2014": "Rocky Mountain Regional Contest (RMRC) 2014",
        "2015": "Rocky Mountain Regional Contest (RMRC) 2015",
        "2016": "Rocky Mountain Regional Contest (RMRC) 2016",
        "2017": "Rocky Mountain Regional Contest (RMRC) 2017",
        "2018": "Rocky Mountain Regional Programming Contest 2018",
        "2019": "Rocky Mountain Regional Programming Contest 2019",
        "2020": "Rocky Mountain Regional Programming Contest 2020",
        "2021": "Rocky Mountain Regional Programming Contest 2021",
        "2023": "Rocky Mountain Regional Programming Contest 2023",
        "2024": "Rocky Mountain Regional Programming Contest 2024",
        "2025": "Rocky Mountain Regional Programming Contest 2025",
    },
    "pacnw": {
        "2015": "2015-2016 ACM-ICPC Pacific Northwest Regional Contest (Div. 1)",
    },
    "ecna": {
        "2015": "2015 ICPC East-Central NA Regional Contest",
        "2016": "2016 ICPC East-Central NA Regional Contest",
        "2017": "2017 ICPC East-Central NA Regional Contest",
        "2018": "2018 ICPC East-Central NA Regional Contest",
        "2019": "2019 ICPC East-Central NA Regional Contest",
        "2020": "2020 ICPC East-Central NA Regional Contest",
        "2021": "2021 ICPC East-Central NA Regional Contest",
        # Aligned to shared 2022 set
        "2022": "2022 ICPC North America Regional Contests (February 25, 2023)",
        "2023": "2023 ICPC North America Regional Programming Contests (February 24, 2024)",
        "2024": "2024 ICPC East Central North America Regional",
        "2025": "2025 ICPC South Central, South East and Mid Atlantic Regional (November 8, 2025)"
    },
    "mcpc": {
        "2014": "ACM/ICPC Mid-Central USA 2014 Regional Contest",
        "2015": "2015 ICPC Mid-Central Regional",
        "2016": "2016 ICPC Mid-Central Regional",
        "2017": "2017 ICPC Mid-Central Regional",
        "2018": "2018 ICPC Mid-Central Regional",
        "2019": "2019 ICPC Mid-Central Regional",
        # Mapped to Shared North American Regional problem sets
        "2025": "2025 ICPC North America Central Division"
    },
    "midatl": {
        "2016": "2016 Mid-Atlantic Regional",
        "2017": "2017 ICPC Mid-Atlantic Regional Contest",
        "2018": "2018 ICPC Mid-Atlantic Regional Contest",
        # Mapped to Shared North American Regional problem sets
        "2020": "2020 ICPC North America Regionals (March 6 2021)",
        "2021": "2021 ICPC North America Regionals (February 26 2022)",
        "2022": "2022 ICPC North America Regional Contests (February 25, 2023)",
        "2023": "2023 ICPC North America Regional Programming Contests (February 24, 2024)",
        "2024": "2024 ICPC South Central, South East and Mid Atlantic Regional (November 16, 2024)",
        "2025": "2025 ICPC South Central, South East and Mid Atlantic Regional (November 8, 2025)"
    },
    "ncna": {
        "2017": "2017 ACM ICPC North Central North America Regional Contest",
        "2018": "2018 ACM-ICPC North Central North America Regional Contest",
        "2023": "North Central North America Regional Contest 2023",
        "2025": "ICPC NCNA Regionals 2025 2026",
    },
    "ser": {
        "2014": "2014 Southeast USA Regionals Division 1",
        "2015": "2015 Southeast USA Regionals Division 1",
        "2016": "2016 Southeast USA Regionals Division 1",
        "2019": "2019 ICPC Southeast USA Regional Programming Contest",
        # Mapped to Shared North American Regional problem sets
        "2020": "2020 ICPC North America Regionals (March 6 2021)",
        "2021": "2021 ICPC North America Regionals (February 26 2022)",
        "2022": "2022 ICPC North America Regional Contests (February 25, 2023)",
        "2023": "2023 ICPC North America Regional Programming Contests (February 24, 2024)",
        "2024": "2024 ICPC South Central, South East and Mid Atlantic Regional (November 16, 2024)",
        "2025": "2025 ICPC South Central, South East and Mid Atlantic Regional (November 8, 2025)"
    },
    "socal": {
        "2019": "2019 ICPC Southern California Regional Programming Contest",
        "2021": "2021/2022 Southern California Regional ICPC",
    },
    "scusa": {
        "2018": "2018 ICPC South Central USA Regional Contest",
        # Mapped to Shared North American Regional problem sets
        "2020": "2020 ICPC North America Regionals (March 6 2021)",
        "2021": "2021 ICPC North America Regionals (February 26 2022)",
        "2022": "2022 ICPC North America Regional Contests (February 25, 2023)",
        "2023": "2023 ICPC North America Regional Programming Contests (February 24, 2024)",
        "2024": "2024 ICPC South Central, South East and Mid Atlantic Regional (November 16, 2024)",
        "2025": "2025 ICPC South Central, South East and Mid Atlantic Regional (November 8, 2025)"
    },
    "naeast": {
        "2023": "2023 North America East Division Contest",
        "2024": "2024 North America East Division Contest",
        "2025": "2025 North America East Division Contest"
    },
    "nane": {
        "2019": "NENA 2019",
        "2023": "2023 North America East Division Contest",
        "2024": "2024 North America East Division Contest",
        "2025": "2025 North America East Division Contest"
    },
    "gnyr": {
        # Aligned to shared 2022 set
        "2022": "2022 ICPC North America Regional Contests (February 25, 2023)",
        "2023": "2023 North America East Division Contest",
        "2024": "2024 North America East Division Contest",
        "2025": "2025 North America East Division Contest"
    }
}

# vanb judging archives
vanb_judging_links = {
    "naq": {
        "2022": "http://serjudging.vanb.org/?cat=42",
        "2023": "http://serjudging.vanb.org/?cat=46",
        "2024": "http://serjudging.vanb.org/?cat=49",
        "2025": "http://serjudging.vanb.org/?cat=52",
        "2026": "http://serjudging.vanb.org/?cat=54"
    },
    "ser": {
        "2012": "http://serjudging.vanb.org/ser2012/",
        "2013": "http://serjudging.vanb.org/ser2013/",
        "2014": "http://serjudging.vanb.org/ser2014/",
        "2015": "http://serjudging.vanb.org/ser2015/",
        "2016": "http://serjudging.vanb.org/ser2016/",
        "2017": "http://serjudging.vanb.org/ser2017/",
        "2018": "http://serjudging.vanb.org/ser2018/",
        "2019": "http://serjudging.vanb.org/ser2019/",
        "2020": "http://serjudging.vanb.org/ser2020/",
        "2021": "http://serjudging.vanb.org/ser2021/",
        "2022": "http://serjudging.vanb.org/ser2022/",
        "2023": "http://serjudging.vanb.org/ser2023/"
    },
    "nadc": {
        "2021": "http://serjudging.vanb.org/nadc2021/"
    },
    "nac": {
        "2020": "http://serjudging.vanb.org/?cat=36",
        "2021": "http://serjudging.vanb.org/?cat=39",
        "2022": "http://serjudging.vanb.org/?cat=41",
        "2023": "http://serjudging.vanb.org/?cat=45",
        "2024": "http://serjudging.vanb.org/?cat=48",
        "2025": "http://serjudging.vanb.org/?cat=51"
    },
    "naipc": {
        "2012": "http://serjudging.vanb.org/?cat=19",
        "2013": "http://serjudging.vanb.org/?cat=22",
        "2014": "http://serjudging.vanb.org/?cat=24",
        "2015": "http://serjudging.vanb.org/?cat=26",
        "2016": "http://serjudging.vanb.org/?cat=28",
        "2017": "http://serjudging.vanb.org/?cat=31",
        "2018": "http://serjudging.vanb.org/?cat=33",
        "2019": "http://serjudging.vanb.org/?cat=34"
    }
}

# QoJ contest IDs (qoj.ac/contest/[ID])
qoj_contest_ids = {
    "nwerc": {
        "2022": "1044"
    },
    "cerc": {
        "2022": "1070"
    }
}

# Year-specific official archive URLs (such as Wayback Machine backups)
contest_other_year_links = {
    "naq": {
        "2012": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2012/",
        "2013": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2013/",
        "2014": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2014/",
        "2015": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2015/",
        "2016": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2016/",
        "2017": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2017/",
        "2018": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2018/",
        "2019": "https://cs.baylor.edu/~hamerly/icpc/qualifier_2019/",
        "2020": "https://web.archive.org/web/20210625140009/https://www.icpc.org/icpc-north-america-qualifier",
        "2021": "https://na.icpc.global/2021-22/naq/"
    },
    "gnyr": {
        "2025": "https://east.na.icpc.global/"
    },
    "naeast": {
        "2025": "https://east.na.icpc.global/problems/2025"
    },
    "nane": {
        "2025": "https://east.na.icpc.global/problems/2025"
    },
    "ecna": {
        "2021": "https://github.com/icpc/na-ecna-archive",
        "2022": "https://github.com/icpc/na-ecna-archive",
        "2023": "https://github.com/icpc/na-ecna-archive",
        "2025": "https://east.na.icpc.global/problems/2025"
    }
}

# Load the comprehensive crawled solved.ac database
crawled_contests_json_path = os.path.join(os.path.dirname(__file__), "crawled_contests.json")
crawled_data = {}
if os.path.exists(crawled_contests_json_path):
    try:
        with open(crawled_contests_json_path, "r", encoding="utf-8") as f:
            crawled_data = json.load(f)
    except Exception:
        pass

# Explicit mapped solved.ac IDs for each year/contest! (These are mapped directly to their actual BOJ/solved.ac archives)
solved_ac_contest_ids = {
    # Benelux (bapc)
    "bapc": {
        "2012": "108",
        "2013": "134",
        "2018": "1937",
        "2019": "2094",
        "2020": "2381",
        "2021": "2854",
        "2022": "3214",
        "2023": "4010",
        "2024": "4319"
    },
    # Germany (gcpc)
    "gcpc": {
        "2015": "163",
        "2022": "3444",
        "2023": "3944"
    },
    # Nordic (ncpc)
    "ncpc": {
        "2014": "176",
        "2016": "216",
        "2022": "3514",
        "2023": "3984"
    },
    # Europe Regionals
    "nwerc": {
        "2022": "3211",
        "2023": "3962"
    },
    "cerc": {
        "2022": "3220",
        "2021": "2840"
    },
    "swerc": {
        "2023": "3833"
    },
    "seerc": {
        "2022": "3192",
        "2019": "2100"
    },
    "naipc": {
        "2012": "428",
        "2013": "854",
        "2014": "1282",
        "2015": "1387",
        "2016": "1502",
        "2017": "1733",
        "2018": "1906",
        "2019": "2003"
    }
}

solved_ac_keywords_map = {
    "bapc": ["Benelux Algorithm Programming Contest", "BAPC"],
    "bapc-prelim": ["BAPC Preliminaries", "BAPC 2005 Preliminaries", "BAPC 2007 Preliminaries", "BAPC 2008 Preliminaries", "BAPC 2010 Preliminaries", "BAPC 2011 Preliminaries", "BAPC 2012 Preliminaries", "BAPC 2013 Preliminaries", "BAPC 2014 Preliminaries", "BAPC 2015 Preliminaries", "BAPC 2016 Preliminaries", "BAPC 2017 Preliminaries", "BAPC 2018 Preliminaries", "BAPC 2019 Preliminaries", "BAPC 2020 Preliminaries", "BAPC 2021 Preliminaries", "BAPC 2022 Preliminaries", "BAPC 2023 Preliminaries", "BAPC 2024 Preliminaries", "BAPC 2025 Preliminaries", "preliminaries"],
    "gcpc": ["German Collegiate Programming Contest", "GCPC"],
    "ncpc": ["Nordic Collegiate Programming Contest", "NCPC"],
    "nwerc": ["Northwestern Europe", "NWERC"],
    "cerc": ["Central Europe", "CERC"],
    "swerc": ["Southwestern Europe", "SWERC"],
    "seerc": ["Southeastern Europe", "SEERC"],
    "ukiepc": ["UK & Ireland", "UKIEPC"],
    "finals": ["World Finals"],
    "naq": ["North American Qualifier", "North America Qualifier", "NAQ"],
    "nac": ["North America Championship", "North American Championship", "NAC"],
    "nadc": ["Division Championships", "NADC"],
    "naipc": ["North American Invitational", "NAIPC", "UCIPC", "Chicago Invitational"],
    "midatl": ["Mid-Atlantic"],
    "ser": ["Southeast USA"],
    "naeast": ["North America East", "East Division", "NA East"],
    "nane": ["Northeast North America", "NANE", "NENA"],
    "gnyr": ["Greater New York"],
    "ecna": ["East Central", "East-Central", "ECNA"],
    "mcpc": ["Mid-Central", "Mid-Central USA", "MCPC"],
    "scusa": ["South Central", "South Central USA", "SCUSA"],
    "rmrc": ["Rocky Mountain", "RMRC"],
    "pacnw": ["Pacific Northwest"],
    "socal": ["Southern California", "SoCal"],
    "ncna": ["North Central", "NCNA"],
    "grcpc": ["Greek Collegiate", "GRCPC"]
}

def find_resolved_solved_ac_id(contest, year):
    """
    Search the loaded 904 crawled contests to find the exact database ID
    associated with this contest and year.
    """
    # Prefer explicit overrides if defined in solved_ac_contest_ids
    if contest in solved_ac_contest_ids and year in solved_ac_contest_ids[contest]:
        return solved_ac_contest_ids[contest][year]
        
    if contest not in solved_ac_keywords_map or not crawled_data:
        return None
        
    kws = solved_ac_keywords_map[contest]
    for cid, info in crawled_data.items():
        name = info["name"]
        if year not in name:
            continue
        matches_kw = False
        for kw in kws:
            if kw.lower() in name.lower():
                matches_kw = True
                break
        if matches_kw:
            if contest == "bapc" and "prelim" in name.lower():
                continue
            if contest == "bapc-prelim" and "prelim" not in name.lower():
                continue
            return cid
    return None

def analyze_year(ydir):
    analysis = {
        "problem_statements": False,
        "judge_data": False,
        "judge_solutions": False,
        "solution_description": False,
        "scoreboard_collateral": False,
        "num_problems": 0,
        "problem_list": []
    }
    
    all_files = []
    for root, dirs, files in os.walk(ydir):
        for f in files:
            all_files.append((os.path.join(root, f), f))
            
    non_prob_folders = [
        "board", "report", "results", "standings", "media", "__MACOSX", "prelim", "solutions", 
        "_temp_extract", "html", "icons", "~acm00", "acm.student.cs.uwaterloo.ca", "practice",
        "judge", "team", "testing", "notes", "pdfs", "pdf", "unix", "solDir", "back", "welcome",
        "artwork_NCNA", "IO", "PRACTICE", "pspdf", "input", "output", "in", "out", "input-output",
        "Standings \udce2%80%93 Kattis, North America Qualifier 2014_files"
    ]
    problem_dirs = []
    
    try:
        top_subdirs = [x for x in os.listdir(ydir) if os.path.isdir(os.path.join(ydir, x)) and not x.startswith(".") and x not in non_prob_folders]
    except Exception:
        top_subdirs = []
        
    for d in top_subdirs:
        dpath = os.path.join(ydir, d)
        if d == "data":
            try:
                sub_subs = [x for x in os.listdir(dpath) if os.path.isdir(os.path.join(dpath, x)) and not x.startswith(".")]
                for s in sub_subs:
                    problem_dirs.append(os.path.join(d, s))
            except Exception:
                pass
        else:
            problem_dirs.append(d)
            
    analysis["num_problems"] = len(problem_dirs)
    analysis["problem_list"] = sorted(problem_dirs)
    
    # 1. Problem Statements
    for path, f in all_files:
        lf = f.lower()
        if lf in ["problemset.pdf", "problems.pdf", "pset.pdf", "problemset.html", "problems.html", "statement.pdf", "statement.html", "all.pdf", "real.pdf", "div1.pdf", "div2.pdf", "main_problemset.pdf"]:
            analysis["problem_statements"] = True
            break
        # Broaden match: any PDF starting with 'naq', 'nadc', 'nac', 'naipc', 'nwerc', 'ukiepc', 'seusa', 'scdiv', 'midatlantic', 'ser', 'se', 'scusa', 'ecna', 'mcpc', 'mc_', 'ncna', 'pacnw', 'socal', 'sc20', 'scicpc', 'euc', 'cerc', 'seerc', 'swerc', 'regional_', 'contest2012', 'problems', 'problemset', 'icpc', or matching YYYY.pdf, or containing 'worldfinal', '-ser-', or 'pacnw'
        if lf.endswith(".pdf") and (lf.startswith("naq") or lf.startswith("nadc") or lf.startswith("nac") or lf.startswith("naipc") or lf.startswith("nwerc") or lf.startswith("ukiepc") or lf.startswith("seusa") or lf.startswith("scdiv") or lf.startswith("midatlantic") or lf.startswith("ser") or lf.startswith("se") or lf.startswith("scusa") or lf.startswith("ecna") or lf.startswith("mcpc") or lf.startswith("mc_") or lf.startswith("ncna") or lf.startswith("pacnw") or "pacnw" in lf or lf.startswith("socal") or lf.startswith("sc20") or lf.startswith("scicpc") or lf.startswith("euc") or lf.startswith("cerc") or lf.startswith("seerc") or lf.startswith("swerc") or "problemset_" in lf or lf.startswith("regional_") or lf.startswith("contest2012") or re.match(r'^\d{4}\.pdf$', lf) or "-ser-" in lf or lf.startswith("problems") or lf.startswith("problemset") or lf.startswith("icpc") or "worldfinal" in lf or "problem_statement" in lf):
            analysis["problem_statements"] = True
            break
        if "problem_statement" in path:
            analysis["problem_statements"] = True
            break
            
    if not analysis["problem_statements"]:
        pdf_stmt_count = 0
        for pdir in problem_dirs:
            p_full = os.path.join(ydir, pdir)
            try:
                p_files = os.listdir(p_full) if os.path.isdir(p_full) else []
            except Exception:
                p_files = []
            if any(f.endswith(".pdf") or f.endswith(".html") or f.endswith(".tex") for f in p_files):
                pdf_stmt_count += 1
        if len(problem_dirs) > 0 and pdf_stmt_count >= len(problem_dirs) / 2:
            analysis["problem_statements"] = True
            
    # 2. Judge Data
    for path, f in all_files:
        lf = f.lower()
        if "secret" in path or "test_data" in path or "jury" in path:
            if lf.endswith(".in") or lf.endswith(".ans") or lf.endswith(".out") or lf.endswith(".diff") or lf.endswith(".dat") or lf.endswith(".judge") or re.search(r'\.(in|out|ans)(\.txt|\d*)$', lf):
                analysis["judge_data"] = True
                break
        if (lf.endswith(".in") or lf.endswith(".ans") or lf.endswith(".out") or lf.endswith(".diff") or lf.endswith(".dat") or lf.endswith(".expected") or lf.endswith(".judge") or re.search(r'\.(in|out|ans)(\.txt|\d*)$', lf)) and "sample" not in path:
            analysis["judge_data"] = True
            break
            
    # 3. Judge Solutions
    for path, f in all_files:
        lf = f.lower()
        if "accepted" in path or "submissions" in path or "solutions" in path or "sol" in path or "/cpp" in path or "/java" in path:
            if lf.endswith(".cpp") or lf.endswith(".java") or lf.endswith(".py") or lf.endswith(".c") or lf.endswith(".pas") or lf.endswith(".cpp.txt") or lf.endswith(".python.txt"):
                analysis["judge_solutions"] = True
                break
        if lf.endswith(".cpp") or lf.endswith(".java") or lf.endswith(".py"):
            if "validator" not in lf and "check" not in lf and "make" not in lf:
                analysis["judge_solutions"] = True
                break
                
    # 4. Solution Description
    for path, f in all_files:
        lf = f.lower()
        if "writeup" in lf or "editorial" in lf or "discussion" in lf or "handout" in lf or "slides" in lf or "solution" in lf:
            if lf.endswith(".pdf") or lf.endswith(".ppt") or lf.endswith(".pptx") or lf.endswith(".html"):
                analysis["solution_description"] = True
                break
        if lf in ["solutions.pdf", "solution.pdf", "editorial.pdf", "writeup.pdf", "slides.pdf"]:
            analysis["solution_description"] = True
            break
            
    # 5. Scoreboard & Collateral
    for path, f in all_files:
        lf = f.lower()
        if "scoreboard" in lf or "standings" in lf or "results" in lf or "summary" in lf or "details" in lf or "balloons" in lf:
            analysis["scoreboard_collateral"] = True
            break
            
    return analysis

def format_aligned_table(header, rows):
    num_cols = len(header)
    col_widths = [len(h) for h in header]
    
    for r in rows:
        for i in range(num_cols):
            if i < len(r):
                col_widths[i] = max(col_widths[i], len(r[i]))
                
    header_line = "| " + " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(header)) + " |"
    separator_line = "|-" + "-|-".join("-" * col_widths[i] for i in range(num_cols)) + "-|"
    
    table_lines = [header_line, separator_line]
    for r in rows:
        data_line = "| " + " | ".join(r[i].ljust(col_widths[i]) for i in range(num_cols)) + " |"
        table_lines.append(data_line)
        
    return table_lines

# Load the comprehensive tree-based solved.ac mapping derived from solved_ac_groups.md
custom_solved_ac_json_path = os.path.join(os.path.dirname(__file__), "generated_custom_solved_ac.json")
custom_solved_ac_links = {}
if os.path.isfile(custom_solved_ac_json_path):
    try:
        with open(custom_solved_ac_json_path, "r", encoding="utf-8") as f:
            custom_solved_ac_links = json.load(f)
    except Exception:
        pass


# Dynamically load aligned Kattis problem sources from kattis_sources.md
kattis_sources_from_md = {}
sources_md_path = os.path.join(data_root, "kattis_sources.md")
if os.path.isfile(sources_md_path):
    try:
        with open(sources_md_path, "r", encoding="utf-8", errors="replace") as f:
            md_text = f.read()
        rows = re.findall(r'\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*`([^`]+)`\s*\|\s*\[Open\.Kattis\]\((https?://[^)]+)\)\s*\|', md_text)
        for sname, cname, acronyms_str, url in rows:
            acronyms = re.findall(r'`([^`]+)`', acronyms_str)
            if not acronyms:
                acronyms = [a.strip(' `') for a in acronyms_str.split(',')]
            all_years = re.findall(r'\b(19\d\d|20[0-2]\d)\b', sname)
            for acr in acronyms:
                acr = acr.strip()
                for yr in all_years:
                    kattis_sources_from_md.setdefault(acr, {})[yr] = sname
    except Exception as e:
        print(f"Warning: could not parse kattis_sources.md: {e}")

def format_report_source_links(contest, year):
    """
    Returns separate strings for Kattis, Open.Kattis, solved.ac, and Other.
    If none exist, returns "-" for that specific platform column.
    """
    kattis_link = "-"
    open_kattis_link = "-"
    solved_ac_link = "-"
    other_links = []
    
    # 1. Custom Kattis Contest URL
    if contest in contest_kattis_custom and year in contest_kattis_custom[contest]:
        kattis_link = f"[Kattis]({contest_kattis_custom[contest][year]})"
        
    # 2. Kattis Problem Source URL (open.kattis.com/problem-sources)
    source_name = None
    if contest in kattis_sources_from_md and year in kattis_sources_from_md[contest]:
        source_name = kattis_sources_from_md[contest][year]
    elif contest in kattis_sources_crawled and year in kattis_sources_crawled[contest]:
        source_name = kattis_sources_crawled[contest][year]

    if source_name:
        escaped = urllib.parse.quote(source_name)
        open_kattis_link = f"[Open.Kattis](https://open.kattis.com/problem-sources/{escaped})"

    # 3. Solved.ac Record URL
    if contest in custom_solved_ac_links and year in custom_solved_ac_links[contest]:
        solved_ac_link = custom_solved_ac_links[contest][year]
    else:
        resolved_id = find_resolved_solved_ac_id(contest, year)
        if resolved_id:
            solved_ac_link = f"[solved.ac](https://solved.ac/problems/archive/contest/{resolved_id})"
        
    # 4. Vanb Judging URL (falls under Other)
    if contest in vanb_judging_links and year in vanb_judging_links[contest]:
        other_links.append(f"[Vanb]({vanb_judging_links[contest][year]})")
        
    # 5. QoJ Contest URL (falls under Other)
    if contest in qoj_contest_ids and year in qoj_contest_ids[contest]:
        qoj_id = qoj_contest_ids[contest][year]
        other_links.append(f"[QoJ](https://qoj.ac/contest/{qoj_id})")

    # 6. Year-specific other custom links (falls under Other)
    if contest in contest_other_year_links and year in contest_other_year_links[contest]:
        url = contest_other_year_links[contest][year]
        if "hamerly" in url:
            other_links.append(f"[Hamerly]({url})")
        else:
            other_links.append(f"[Official-Site]({url})")

    # 7. Year official site fallbacks (falls under Other)
    if year.isdigit() and len(year) == 4:
        yr_int = int(year)
        if contest == "nwerc" and yr_int >= 2005 and yr_int <= 2026:
            other_links.append(f"[NWERC](https://{year}.nwerc.eu/)")
        elif contest in ["bapc", "bapc-prelim"] and yr_int >= 2005 and yr_int <= 2026:
            other_links.append(f"[BAPC](https://{year}.bapc.eu/)")
        elif contest == "ncpc" and yr_int >= 2005 and yr_int <= 2026:
            other_links.append(f"[NCPC](https://nordic.icpc.io/ncpc{year}/)")
            
    other_str = " \\| ".join(other_links) if other_links else "-"
    
    return kattis_link, open_kattis_link, solved_ac_link, other_str

def generate_report():
    out_lines = []
    out_lines.append("# ICPC Contest Problems and Judge Data Inventory")
    out_lines.append("")
    out_lines.append("This inventory maps out all active contests and years within the `data/` directory, summarizing the availability of problem statements, judge data, solutions, editorials, and scoreboards.")
    out_lines.append("")
    out_lines.append("## Global Archives & General Resources")
    out_lines.append("")
    out_lines.append("* https://icpcarchive.github.io/")
    out_lines.append("* https://www.cs.nthu.edu.tw/~progcont/ACM/")
    out_lines.append("* https://github.com/jfresen/icpc")
    out_lines.append("* https://codeforces.com/blog/entry/71806")
    out_lines.append("")
    out_lines.append("")

    contests = sorted([d for d in os.listdir(data_root) if d not in ignored_dirs and not d.startswith(".") and os.path.isdir(os.path.join(data_root, d))])
    
    for c in contests:
        c_full = contest_names.get(c, c.upper())
        c_lang = contest_languages.get(c, "Unknown")
        out_lines.append(f"## {c_full} (`{c}`)")
        out_lines.append("")
        out_lines.append(f"**Primary Contest Language:** {c_lang}")
        out_lines.append("")
        
        # General resources
        gen_links = contest_general_links.get(c, [])
        if gen_links:
            out_lines.append("**General Resources & Archives:**")
            for gl in gen_links:
                out_lines.append(f"* {gl}")
            out_lines.append("")
            
        cpath = os.path.join(data_root, c)
        
        sub_items = []
        try:
            sub_items = sorted([sub for sub in os.listdir(cpath) if os.path.isdir(os.path.join(cpath, sub)) and not sub.startswith(".")])
        except Exception:
            pass
            
        # Determine the years list to report, utilizing explicitly defined start years to fill in tables even if folder is empty!
        years_to_report = []
        numeric_years = sorted([int(sub) for sub in sub_items if sub.isdigit() and len(sub) == 4])
        
        if c in contest_start_years:
            start_yr = contest_start_years[c]
            for yr in range(start_yr, 2027):
                years_to_report.append(str(yr))
            # Also add non-numeric subdirs
            non_numeric = [sub for sub in sub_items if not (sub.isdigit() and len(sub) == 4)]
            years_to_report.extend(sorted(non_numeric))
        elif numeric_years:
            min_year = min(numeric_years)
            for yr in range(min_year, 2027):
                years_to_report.append(str(yr))
            non_numeric = [sub for sub in sub_items if not (sub.isdigit() and len(sub) == 4)]
            years_to_report.extend(sorted(non_numeric))
        else:
            years_to_report = sub_items
            
        if not years_to_report and c in ["acpc", "amppz", "ctuopen", "hcpc", "adabyron", "itacpc", "miup", "grcpc", "ukr", "trcpc", "nane", "euc", "bapc-prelim"]:
            out_lines.append("*Empty contest directory.*")
            out_lines.append("")
            continue
            
        header = ["Year", "Problems", "Statements", "Judge Data", "Solutions", "Editorial", "Scoreboard/Collateral", "Kattis", "Open.Kattis", "solved.ac", "Other", "Status/Notes"]
        table_rows = []
        
        for y in years_to_report:
            ypath = os.path.join(cpath, y)
            is_link = os.path.islink(ypath)
            exists = os.path.exists(ypath) or is_link
            
            kattis_url, open_kattis_url, solved_ac_url, other_url = format_report_source_links(c, y)
            
            if exists:
                real_ypath = os.path.realpath(ypath)
                analysis = analyze_year(real_ypath)
                
                stmt_ok = "Yes" if analysis["problem_statements"] else "No"
                data_ok = "Yes" if analysis["judge_data"] else "No"
                sol_ok = "Yes" if analysis["judge_solutions"] else "No"
                edit_ok = "Yes" if analysis["solution_description"] else "No"
                sc_ok = "Yes" if analysis["scoreboard_collateral"] else "No"
                
                num_probs = str(analysis["num_problems"])
                notes = []
                if is_link:
                    target_rel = os.path.relpath(real_ypath, data_root)
                    notes.append(f"Linked to {target_rel}")
                elif c == "gnyr" and y == "2023":
                    notes.append("Shared 2023 East Division problem set with naeast")
                if analysis["num_problems"] > 0:
                    notes.append(f"{analysis['num_problems']} problems")
                else:
                    notes.append("0 problems found")
                notes_str = ", ".join(notes)
            else:
                stmt_ok = "No"
                data_ok = "No"
                sol_ok = "No"
                edit_ok = "No"
                sc_ok = "No"
                num_probs = "0"
                notes_str = "Missing"
                
            table_rows.append([
                y,
                num_probs,
                stmt_ok,
                data_ok,
                sol_ok,
                edit_ok,
                sc_ok,
                kattis_url,
                open_kattis_url,
                solved_ac_url,
                other_url,
                notes_str
            ])
            
        if table_rows:
            table_lines = format_aligned_table(header, table_rows)
            out_lines.extend(table_lines)
        else:
            out_lines.append("*No data available.*")
            
        out_lines.append("")
        
    report_path = os.path.join(data_root, "report.md")
    with open(report_path, "w", encoding="utf-8") as rf:
        rf.write("\n".join(out_lines))
    print(f"Generated aligned report with split link columns at {report_path}!")

if __name__ == "__main__":
    generate_report()
