from __future__ import annotations

from pathlib import Path
import shutil
import os
import json
import re
import markdown
import struct
from urllib.parse import parse_qs, quote_plus, urlparse

import settings_config
import validate

from jinja2 import Environment, FileSystemLoader


ROOT_DIR = Path(__file__).resolve().parent.parent

CONTENT_DIR = settings_config.active_content_dir()
OUTPUT_DIR = ROOT_DIR / "ausgabe"

GENERATOR_DIR = ROOT_DIR / "generator"

TEMPLATE_DIR = GENERATOR_DIR / "templates"
STATIC_DIR = GENERATOR_DIR / "static"
COMPONENTS_FILE = GENERATOR_DIR / "components.json"
SETTINGS_FILE = GENERATOR_DIR / "settings.json"
SETTINGS_MARKDOWN_FILE = settings_config.active_settings_file() or settings_config.SETTINGS_MARKDOWN_FILE

MEDIA_DIR_NAME = "medien"
LEGACY_MEDIA_DIR_NAME = "assets"
GALLERY_DIR_NAME = "galerie"
LEGACY_GALLERY_DIR_NAME = "gallery"
FOOTER_DIR_NAME = "fusszeile"
LEGACY_FOOTER_DIR_NAME = "footer"
FOOTER_FILE_NAME = "fusszeile.md"
LEGACY_FOOTER_FILE_NAME = "footer.md"

IMAGE_EXTENSIONS = {
    ".jpg",
    ".jpeg",
    ".png",
    ".gif",
    ".webp",
    ".svg"
}

VIDEO_EXTENSIONS = {
    ".mp4",
    ".webm",
    ".ogg",
    ".mov"
}


env = Environment(
    loader=FileSystemLoader(TEMPLATE_DIR)
)


with open(COMPONENTS_FILE, encoding="utf-8") as f:
    COMPONENTS = json.load(f)


DEFAULT_SETTINGS = settings_config.DEFAULT_SETTINGS


def merge_settings(
    defaults: dict,
    custom: dict
) -> dict:
    merged = defaults.copy()

    for key, value in custom.items():
        if (
            isinstance(value, dict)
            and isinstance(merged.get(key), dict)
        ):
            merged[key] = merge_settings(
                merged[key],
                value
            )
        else:
            merged[key] = value

    return merged


def load_settings() -> dict:
    return settings_config.load_settings()


SETTINGS = DEFAULT_SETTINGS


def run_validation(format_settings: bool = True):
    messages, language = validate.collect_messages(format_settings=format_settings)

    if not messages:
        print(validate.tr(language, "ok"))
        return

    print(validate.tr(language, "found_header"))

    for message in messages:
        print(f"- {message}")

    print(validate.tr(language, "found_footer"))

    if validate.has_errors(messages):
        raise SystemExit(1)


def normalize_component(name: str) -> str:
    return COMPONENTS.get(name, name)


def parse_annotation(line: str) -> tuple[str, str | None]:
    annotation = line.strip()[1:]

    if ":" in annotation:
        component, parameter = annotation.split(":", 1)

        return (
            normalize_component(component.strip()),
            parameter.strip()
        )

    return (
        normalize_component(annotation.strip()),
        None
    )


def parse_blocks(md_text: str) -> list[dict]:
    blocks = []

    current_type = "text"
    current_parameter = None
    current_lines = []

    for line in md_text.splitlines():

        if line.strip().startswith("@"):

            if current_lines:
                blocks.append({
                    "type": current_type,
                    "parameter": current_parameter,
                    "content": "\n".join(current_lines).strip()
                })

            current_type, current_parameter = parse_annotation(line)

            current_lines = []

        else:
            current_lines.append(line)

    if current_lines:
        blocks.append({
            "type": current_type,
            "parameter": current_parameter,
            "content": "\n".join(current_lines).strip()
        })

    return [
        block for block in blocks
        if block["content"]
    ]


def render_markdown(md_text: str) -> str:
    return markdown.markdown(
        md_text,
        extensions=["extra"]
    )


def render_inline_markdown(md_text: str) -> str:
    html = render_markdown(md_text.strip())
    match = re.fullmatch(r"<p>(.*)</p>", html, re.S)

    return match.group(1) if match else html


def split_h2_blocks(md_text: str):
    lines = md_text.splitlines()

    title_lines = []
    blocks = []
    current = []

    for line in lines:

        if line.startswith("## "):

            if current:
                blocks.append(
                    "\n".join(current).strip()
                )

            current = [line]

        else:

            if current:
                current.append(line)

            else:
                title_lines.append(line)

    if current:
        blocks.append(
            "\n".join(current).strip()
        )

    return (
        "\n".join(title_lines).strip(),
        blocks
    )


def extract_images(md_text: str) -> list[dict]:
    pattern = r"!\[(.*?)\]\((.*?)\)"

    images = []

    for alt, src in re.findall(pattern, md_text):

        images.append({
            "alt": alt.strip(),
            "src": src.strip()
        })

    return images


def remove_markdown_images(md_text: str) -> str:
    return re.sub(
        r"!\[(.*?)\]\((.*?)\)",
        "",
        md_text
    ).strip()


def remove_first_image(md_text: str):

    pattern = r"!\[(.*?)\]\((.*?)\)"

    match = re.search(pattern, md_text)

    if not match:
        return None, md_text

    image = {
        "alt": match.group(1).strip(),
        "src": match.group(2).strip()
    }

    remaining = (
        md_text[:match.start()]
        + md_text[match.end():]
    )

    return image, remaining.strip()


def extract_links(md_text: str) -> list[dict]:
    pattern = r"\[(.*?)\]\((.*?)\)"

    links = []

    for label, href in re.findall(pattern, md_text):
        links.append({
            "label": label.strip(),
            "href": href.strip()
        })

    return links


def key_value_lines(md_text: str) -> tuple[dict, str]:
    values = {}
    remaining_lines = []

    for line in md_text.splitlines():
        match = re.match(r"^([A-Za-zÄÖÜäöüß _-]+):\s*(.+)$", line)

        if match:
            key = slugify_title(match.group(1)).replace("-", "_")
            values[key] = match.group(2).strip()
        else:
            remaining_lines.append(line)

    return values, "\n".join(remaining_lines).strip()


def href_from_output(
    current_output_file: Path,
    target_output_file: Path
) -> str:

    current_dir = current_output_file.parent

    relative_href = os.path.relpath(
        target_output_file,
        start=current_dir
    )

    return relative_href.replace("\\", "/")


def content_path_from_src(src: str) -> Path:
    return CONTENT_DIR / src


def output_path_from_src(src: str) -> Path:
    return OUTPUT_DIR / src


def jpeg_dimensions(file_path: Path) -> tuple[int, int] | None:
    with file_path.open("rb") as f:
        if f.read(2) != b"\xff\xd8":
            return None

        while True:
            marker_start = f.read(1)

            if not marker_start:
                return None

            if marker_start != b"\xff":
                continue

            marker = f.read(1)

            while marker == b"\xff":
                marker = f.read(1)

            if marker in {
                b"\xc0",
                b"\xc1",
                b"\xc2",
                b"\xc3",
                b"\xc5",
                b"\xc6",
                b"\xc7",
                b"\xc9",
                b"\xca",
                b"\xcb",
                b"\xcd",
                b"\xce",
                b"\xcf"
            }:
                f.read(3)
                height, width = struct.unpack(">HH", f.read(4))
                return width, height

            segment_length_data = f.read(2)

            if len(segment_length_data) != 2:
                return None

            segment_length = struct.unpack(
                ">H",
                segment_length_data
            )[0]

            f.seek(segment_length - 2, os.SEEK_CUR)


def image_dimensions(file_path: Path) -> tuple[int, int] | None:
    try:
        with file_path.open("rb") as f:
            header = f.read(24)

        if header.startswith(b"\x89PNG\r\n\x1a\n"):
            width, height = struct.unpack(">II", header[16:24])
            return width, height

        if header[:6] in {b"GIF87a", b"GIF89a"}:
            width, height = struct.unpack("<HH", header[6:10])
            return width, height

        if header.startswith(b"\xff\xd8"):
            return jpeg_dimensions(file_path)

    except OSError:
        return None

    return None


def fallback_image_size() -> dict:
    return {
        "width": 1600,
        "height": 1000
    }


def image_size_for_content_relative(src: str) -> dict:
    src = src.split("?", 1)[0].split("#", 1)[0]

    if is_external_url(src):
        return fallback_image_size()

    file_path = CONTENT_DIR / src

    if not file_path.exists():
        return fallback_image_size()

    dimensions = image_dimensions(file_path)

    if not dimensions:
        return fallback_image_size()

    width, height = dimensions

    return {
        "width": width,
        "height": height
    }


def is_external_url(src: str) -> bool:
    return (
        src.startswith("http://")
        or src.startswith("https://")
    )


def media_src_variants(src: str) -> list[str]:
    variants = [src]

    if src.startswith(f"{LEGACY_MEDIA_DIR_NAME}/"):
        variants.append(
            f"{MEDIA_DIR_NAME}/{src.removeprefix(f'{LEGACY_MEDIA_DIR_NAME}/')}"
        )

    variants = [
        variant.replace(
            f"{MEDIA_DIR_NAME}/{LEGACY_GALLERY_DIR_NAME}/",
            f"{MEDIA_DIR_NAME}/{GALLERY_DIR_NAME}/"
        )
        for variant in variants
    ]

    return list(dict.fromkeys(variants))


def find_asset_by_name(file_name: str) -> Path | None:
    media_dirs = [
        CONTENT_DIR / MEDIA_DIR_NAME,
        CONTENT_DIR / LEGACY_MEDIA_DIR_NAME
    ]

    existing_dirs = [
        media_dir for media_dir in media_dirs
        if media_dir.exists()
    ]

    if not existing_dirs:
        return None

    matches = []

    for media_dir in existing_dirs:
        matches.extend([
            path for path in media_dir.rglob(file_name)
            if path.is_file()
        ])

    matches = sorted(matches)

    if not matches:
        return None

    return matches[0]


def content_relative_from_src(
    src: str,
    source_file: Path | None = None
) -> str:
    src = src.strip().strip("/")

    candidates = []

    for variant in media_src_variants(src):

        if source_file:
            candidates.append(source_file.parent / variant)

        candidates.append(CONTENT_DIR / variant)

    for candidate in candidates:
        candidate = candidate.resolve()

        if not candidate.exists():
            continue

        try:
            return candidate.relative_to(CONTENT_DIR).as_posix()
        except ValueError:
            pass

    fallback = find_asset_by_name(Path(src).name)

    if fallback:
        return fallback.relative_to(CONTENT_DIR).as_posix()

    return src


def image_src_for_page(
    src: str,
    current_output_file: Path,
    source_file: Path | None = None
) -> str:

    content_relative = content_relative_from_src(
        src,
        source_file
    )

    target_output_file = output_path_from_src(
        content_relative
    )

    return href_from_output(
        current_output_file,
        target_output_file
    )


def collect_gallery_images(
    folder_name: str,
    current_output_file: Path
) -> list[dict]:

    source_folder = CONTENT_DIR / MEDIA_DIR_NAME / GALLERY_DIR_NAME / folder_name

    if not source_folder.exists():
        source_folder = (
            CONTENT_DIR
            / LEGACY_MEDIA_DIR_NAME
            / LEGACY_GALLERY_DIR_NAME
            / folder_name
        )

    if not source_folder.exists():
        return []

    images = []

    for file in sorted(source_folder.iterdir()):

        if (
            file.is_file()
            and file.suffix.lower() in IMAGE_EXTENSIONS
        ):

            content_relative = file.relative_to(
                CONTENT_DIR
            ).as_posix()

            image_src = image_src_for_page(
                content_relative,
                current_output_file
            )

            images.append({
                "alt": "",
                "src": image_src,
                **image_size_for_content_relative(
                    content_relative
                )
            })

    return images


def resolve_manual_image_paths(
    images: list[dict],
    current_output_file: Path,
    source_file: Path
) -> list[dict]:

    resolved_images = []

    for image in images:

        src = image["src"]

        if is_external_url(src):

            resolved_src = src
            size = fallback_image_size()

        else:
            content_relative = content_relative_from_src(
                src,
                source_file
            )

            resolved_src = image_src_for_page(
                content_relative,
                current_output_file,
                None
            )

            size = image_size_for_content_relative(
                content_relative
            )

        resolved_images.append({
            "alt": image["alt"],
            "src": resolved_src,
            **size
        })

    return resolved_images


def card_layout(parameter: str) -> str:
    normalized = slugify_title(parameter or "")

    return {
        "einspaltig": "one",
        "eine_spalte": "one",
        "zweispaltig": "two",
        "zwei_spalten": "two",
        "dreispaltig": "three",
        "drei_spalten": "three"
    }.get(normalized, "one")


def render_cards(md_text: str, parameter: str = "") -> str:

    title, cards = split_h2_blocks(md_text)

    template = env.get_template("cards.html")

    return template.render(
        layout=card_layout(parameter),
        title=render_markdown(title),
        cards=[
            render_markdown(card)
            for card in cards
        ]
    )


def render_two_columns(md_text: str) -> str:

    title, columns = split_h2_blocks(md_text)

    left = columns[0] if len(columns) > 0 else ""
    right = columns[1] if len(columns) > 1 else ""

    template = env.get_template("two-columns.html")

    return template.render(
        title=render_markdown(title),
        left=render_markdown(left),
        right=render_markdown(right)
    )


def render_timeline(md_text: str) -> str:

    title, items = split_h2_blocks(md_text)

    template = env.get_template("timeline.html")

    return template.render(
        title=render_markdown(title),
        items=[
            render_markdown(item)
            for item in items
        ]
    )


def render_faq(md_text: str) -> str:

    title, items = split_h2_blocks(md_text)

    template = env.get_template("faq.html")

    return template.render(
        title=render_markdown(title),
        items=[
            render_markdown(item)
            for item in items
        ]
    )


def render_gallery(
    md_text: str,
    current_output_file: Path,
    source_file: Path,
    parameter: str | None
) -> str:

    if parameter:

        images = collect_gallery_images(
            parameter,
            current_output_file
        )

        title_text = md_text.strip()

    else:

        manual_images = extract_images(md_text)

        images = resolve_manual_image_paths(
            manual_images,
            current_output_file,
            source_file
        )

        title_text = remove_markdown_images(md_text)

    template = env.get_template("gallery.html")
    external_content_enabled = SETTINGS.get(
        "privacy",
        {}
    ).get("external_content", False)
    has_external_images = any(
        is_external_url(image["src"])
        for image in images
    )

    return template.render(
        title=render_markdown(title_text),
        images=images,
        gallery=SETTINGS.get("gallery", {}),
        needs_external_consent=has_external_images and not external_content_enabled
    )


def render_image_text(
    md_text: str,
    current_output_file: Path,
    source_file: Path
) -> str:

    image, remaining_text = remove_first_image(md_text)

    if image:

        image["src"] = image_src_for_page(
            image["src"],
            current_output_file,
            source_file
        )

    template = env.get_template("image-text.html")

    return template.render(
        image=image,
        content=render_markdown(remaining_text)
    )


def event_parts(
    md_text: str,
    current_output_file: Path,
    source_file: Path
) -> dict:
    values, remaining_text = key_value_lines(md_text)
    image, remaining_text = remove_first_image(remaining_text)

    if image:
        image["src"] = image_src_for_page(
            image["src"],
            current_output_file,
            source_file
        )

    return {
        "facts": {
            key: render_inline_markdown(value)
            for key, value in values.items()
        },
        "content": render_markdown(remaining_text),
        "image": image
    }


def render_event(
    md_text: str,
    current_output_file: Path,
    source_file: Path
) -> str:
    event = event_parts(md_text, current_output_file, source_file)
    template = env.get_template("event.html")

    return template.render(
        values=event["facts"],
        content=event["content"],
        image=event["image"]
    )


def render_events(
    md_text: str,
    current_output_file: Path,
    source_file: Path
) -> str:
    title, event_blocks = split_h2_blocks(md_text)
    events = []

    for event_block in event_blocks:
        events.append(event_parts(
            event_block,
            current_output_file,
            source_file
        ))

    template = env.get_template("events.html")

    return template.render(
        title=render_markdown(title),
        events=events
    )


def render_downloads(
    md_text: str,
    current_output_file: Path,
    source_file: Path
) -> str:
    title_text = remove_markdown_images(md_text)
    links = []

    for link in extract_links(md_text):
        href = link["href"]

        if not is_external_url(href):
            href = image_src_for_page(
                href,
                current_output_file,
                source_file
            )

        links.append({
            "label": link["label"],
            "href": href
        })

    template = env.get_template("downloads.html")

    return template.render(
        title=render_markdown(re.sub(
            r"\[(.*?)\]\((.*?)\)",
            "",
            title_text
        ).strip()),
        links=links
    )


def render_people(md_text: str) -> str:
    title, people = split_h2_blocks(md_text)
    template = env.get_template("people.html")

    return template.render(
        title=render_markdown(title),
        people=[
            render_markdown(person)
            for person in people
        ]
    )


def render_person(md_text: str) -> str:
    template = env.get_template("person.html")

    return template.render(
        content=render_markdown(md_text)
    )


def render_sponsors(
    md_text: str,
    current_output_file: Path,
    source_file: Path
) -> str:
    images = resolve_manual_image_paths(
        extract_images(md_text),
        current_output_file,
        source_file
    )
    title_text = remove_markdown_images(md_text)
    template = env.get_template("sponsors.html")

    return template.render(
        title=render_markdown(title_text),
        sponsors=images
    )


def youtube_video_id(url: str) -> str | None:
    parsed = urlparse(url)
    host = parsed.netloc.lower().replace("www.", "")
    path_parts = [
        part for part in parsed.path.split("/")
        if part
    ]

    if host == "youtu.be" and path_parts:
        return path_parts[0]

    if host.endswith("youtube.com"):
        if parsed.path == "/watch":
            return parse_qs(parsed.query).get("v", [None])[0]

        if len(path_parts) >= 2 and path_parts[0] in {
            "embed",
            "shorts"
        }:
            return path_parts[1]

    return None


def youtube_embed_url(url: str) -> str | None:
    video_id = youtube_video_id(url)

    if not video_id:
        return None

    return (
        "https://www.youtube.com/embed/"
        f"{video_id}"
        "?autoplay=1"
        "&mute=1"
        "&loop=1"
        f"&playlist={video_id}"
        "&controls=0"
        "&modestbranding=1"
        "&playsinline=1"
    )


def hero_media_from_parameter(
    parameter: str | None,
    current_output_file: Path,
    source_file: Path
) -> dict | None:
    if not parameter:
        return None

    parameter = parameter.strip()
    suffix = Path(urlparse(parameter).path).suffix.lower()

    if is_external_url(parameter):
        embed_url = youtube_embed_url(parameter)

        if embed_url:
            return {
                "type": "youtube",
                "src": embed_url,
                "external_url": parameter,
                "style": ""
            }

        if suffix in IMAGE_EXTENSIONS:
            return {
                "type": "image",
                "src": parameter,
                "style": ""
            }

        if suffix in VIDEO_EXTENSIONS:
            return {
                "type": "video",
                "src": parameter,
                "style": ""
            }

    if suffix in IMAGE_EXTENSIONS:
        return {
            "type": "image",
            "src": image_src_for_page(
                parameter,
                current_output_file,
                source_file
            ),
            "style": ""
        }

    if suffix in VIDEO_EXTENSIONS:
        return {
            "type": "video",
            "src": image_src_for_page(
                parameter,
                current_output_file,
                source_file
            ),
            "style": ""
        }

    return {
        "type": "color",
        "src": "",
        "style": f"background: {parameter};"
    }


def render_hero(
    md_text: str,
    current_output_file: Path,
    source_file: Path,
    parameter: str | None
) -> str:
    template = env.get_template("hero.html")

    return template.render(
        content=render_markdown(md_text),
        external_content_enabled=SETTINGS.get(
            "privacy",
            {}
        ).get("external_content", False),
        hero=hero_media_from_parameter(
            parameter,
            current_output_file,
            source_file
        )
    )


def osm_search_url(address: str) -> str:
    return (
        "https://www.openstreetmap.org/search?query="
        f"{quote_plus(address)}"
    )


def render_location(
    md_text: str,
    parameter: str | None
) -> str:
    address = (parameter or "").strip()
    content = md_text.strip()

    if not address:
        values, remaining_text = key_value_lines(md_text)
        address = values.get("Adresse", values.get("adresse", "")).strip()
        content = remaining_text

    map_url = osm_search_url(address) if address else ""
    template = env.get_template("location.html")

    return template.render(
        address=address,
        content=render_markdown(content),
        map_url=map_url,
        external_content_enabled=SETTINGS.get(
            "privacy",
            {}
        ).get("external_content", False)
    )


def render_footer(
    md_text: str,
    logo: dict | None = None,
    social_links: list[dict] | None = None
) -> str:

    title, columns = split_h2_blocks(md_text)
    columns = [
        column for column in columns
        if not re.match(
            r"(?im)^##\s*(social|soziales|social media)\s*$",
            column.strip()
        )
    ]

    template = env.get_template("footer.html")

    return template.render(
        title=render_markdown(title),
        logo=logo,
        social_links=social_links or [],
        columns=[
            render_markdown(column)
            for column in columns
        ]
    )


def render_block(
    block: dict,
    current_output_file: Path,
    source_file: Path
) -> str:

    block_type = block["type"]
    content = block["content"]
    parameter = block["parameter"]

    if block_type == "cards":
        return render_cards(content, parameter)

    if block_type == "two-columns":
        return render_two_columns(content)

    if block_type == "timeline":
        return render_timeline(content)

    if block_type == "faq":
        return render_faq(content)

    if block_type == "gallery":
        return render_gallery(
            content,
            current_output_file,
            source_file,
            parameter
        )

    if block_type == "image-text":
        return render_image_text(
            content,
            current_output_file,
            source_file
        )

    if block_type == "event":
        return render_event(
            content,
            current_output_file,
            source_file
        )

    if block_type == "events":
        return render_events(
            content,
            current_output_file,
            source_file
        )

    if block_type == "downloads":
        return render_downloads(
            content,
            current_output_file,
            source_file
        )

    if block_type == "person":
        return render_person(content)

    if block_type == "people":
        return render_people(content)

    if block_type == "sponsors":
        return render_sponsors(
            content,
            current_output_file,
            source_file
        )

    if block_type == "location":
        return render_location(
            content,
            parameter
        )

    if block_type == "hero":
        return render_hero(
            content,
            current_output_file,
            source_file,
            parameter
        )

    template_name = f"{block_type}.html"

    if not (TEMPLATE_DIR / template_name).exists():
        template_name = "text.html"

    template = env.get_template(template_name)

    return template.render(
        content=render_markdown(content)
    )


def title_from_file(md_file: Path) -> str:

    stem = display_name(md_file.stem)

    if stem == "index":

        if md_file.parent == CONTENT_DIR:
            return "Start"

        return display_name(md_file.parent.name).replace(
            "-",
            " "
        ).title()

    return stem.replace(
        "-",
        " "
    ).title()


def display_name(name: str) -> str:
    return re.sub(r"^\d+[_-]+", "", name)


def output_path_from_file(md_file: Path) -> Path:

    relative = md_file.relative_to(CONTENT_DIR)
    relative_parts = [
        display_name(part)
        for part in relative.parts
    ]
    relative = Path(*relative_parts)

    if display_name(md_file.stem) == "index":
        return OUTPUT_DIR / relative.parent / "index.html"

    return OUTPUT_DIR / relative.with_suffix(".html")


def is_footer_file(md_file: Path) -> bool:

    relative = md_file.relative_to(CONTENT_DIR)

    return (
        len(relative.parts) >= 2
        and relative.parts[0] in {FOOTER_DIR_NAME, LEGACY_FOOTER_DIR_NAME}
        and md_file.name in {FOOTER_FILE_NAME, LEGACY_FOOTER_FILE_NAME}
    )


def is_settings_file(md_file: Path) -> bool:
    return md_file == SETTINGS_MARKDOWN_FILE


def static_path_from_output(output_file: Path) -> str:

    return href_from_output(
        output_file,
        OUTPUT_DIR / "static" / "style.css"
    )


def logo_for_page(
    settings: dict,
    current_output_file: Path
) -> dict:
    logo = settings.get("logo", {}).copy()

    logo_src = logo.get("src", "").strip().strip("/")
    logo_folder = logo.get("folder", "logo").strip().strip("/") or "logo"

    if not logo_src:
        logo_dir = CONTENT_DIR / MEDIA_DIR_NAME / logo_folder

        if not logo_dir.exists():
            logo_dir = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME / logo_folder

        logo_files = sorted([
            file.name for file in logo_dir.iterdir()
            if file.is_file() and file.suffix.lower() in IMAGE_EXTENSIONS
        ]) if logo_dir.exists() else []

        if logo_files:
            logo_src = f"{MEDIA_DIR_NAME}/{logo_folder}/{logo_files[0]}"

    if logo_src:

        if "/" not in logo_src:
            logo_src = f"{MEDIA_DIR_NAME}/{logo_folder}/{logo_src}"

        logo["url"] = image_src_for_page(
            logo_src,
            current_output_file
        )
    else:
        logo["url"] = ""

    logo["home_url"] = href_from_output(
        current_output_file,
        OUTPUT_DIR / "index.html"
    )

    return logo


def social_for_page(settings: dict) -> list[dict]:
    social = settings.get("social", {})
    icon_style = settings.get("icons", {}).get("style", "round")
    labels = {
        "instagram": "Instagram",
        "youtube": "YouTube",
        "facebook": "Facebook",
        "x": "X",
        "twitter": "Twitter",
        "linkedin": "LinkedIn"
    }
    icons = {
        "instagram": "IG",
        "youtube": "YT",
        "facebook": "FB",
        "x": "X",
        "twitter": "TW",
        "linkedin": "IN"
    }

    return [
        {
            "key": key,
            "label": labels.get(key, key.title()),
            "icon": (
                labels.get(key, key.title())
                if icon_style == "text"
                else icons.get(key, key[:2].upper())
            ),
            "url": url
        }
        for key, url in social.items()
        if url
    ]


def slugify_title(title: str) -> str:

    slug = title.strip().lower()

    slug = slug.replace("ä", "ae")
    slug = slug.replace("ö", "oe")
    slug = slug.replace("ü", "ue")
    slug = slug.replace("ß", "ss")

    slug = re.sub(
        r"[^a-z0-9]+",
        "-",
        slug
    )

    slug = slug.strip("-")

    return slug


def build_page_index() -> dict:

    page_index = {}

    for md_file in CONTENT_DIR.rglob("*.md"):

        if is_footer_file(md_file):
            continue

        if is_settings_file(md_file):
            continue

        key_from_filename = md_file.stem.lower()

        key_from_title = slugify_title(
            title_from_file(md_file)
        )

        target = output_path_from_file(md_file)

        page_index[key_from_filename] = target
        page_index[key_from_title] = target

    return page_index


def resolve_wiki_links(
    text: str,
    current_output_file: Path,
    page_index: dict
) -> str:

    pattern = r"\[\[(.*?)\]\]"

    def replacer(match):

        label = match.group(1).strip()

        key = slugify_title(label)

        if key not in page_index:
            return label

        target_output = page_index[key]

        href = href_from_output(
            current_output_file,
            target_output
        )

        return f"[{label}]({href})"

    return re.sub(pattern, replacer, text)


def build_navigation_raw() -> list[dict]:

    navigation = []

    for md_file in sorted(CONTENT_DIR.glob("*.md")):

        if md_file.name == FOOTER_FILE_NAME:
            continue

        if is_settings_file(md_file):
            continue

        navigation.append({
            "type": "link",
            "title": title_from_file(md_file),
            "target": output_path_from_file(md_file)
        })

    for folder in sorted([
        p for p in CONTENT_DIR.iterdir()
        if p.is_dir()
    ]):

        if folder.name == FOOTER_DIR_NAME:
            continue

        if folder.name in {MEDIA_DIR_NAME, LEGACY_MEDIA_DIR_NAME}:
            continue

        children = []

        for md_file in sorted(folder.glob("*.md")):

            children.append({
                "title": title_from_file(md_file),
                "target": output_path_from_file(md_file)
            })

        if children:

            navigation.append({
                "type": "dropdown",
                "title": display_name(folder.name).replace(
                    "-",
                    " "
                ).title(),
                "children": children
            })

    return navigation


def navigation_for_page(
    navigation_raw: list[dict],
    current_output_file: Path
) -> list[dict]:

    navigation = []

    for item in navigation_raw:

        if item["type"] == "link":

            navigation.append({
                "type": "link",
                "title": item["title"],
                "url": href_from_output(
                    current_output_file,
                    item["target"]
                )
            })

        elif item["type"] == "dropdown":

            children = []

            for child in item["children"]:

                children.append({
                    "title": child["title"],
                    "url": href_from_output(
                        current_output_file,
                        child["target"]
                    )
                })

            navigation.append({
                "type": "dropdown",
                "title": item["title"],
                "children": children
            })

    return navigation


def render_footer_for_page(
    current_output_file: Path,
    page_index: dict
) -> str:

    footer_file = CONTENT_DIR / FOOTER_DIR_NAME / FOOTER_FILE_NAME

    if not footer_file.exists():
        footer_file = CONTENT_DIR / FOOTER_DIR_NAME / LEGACY_FOOTER_FILE_NAME

    if not footer_file.exists():
        footer_file = CONTENT_DIR / LEGACY_FOOTER_DIR_NAME / LEGACY_FOOTER_FILE_NAME

    if not footer_file.exists():
        return ""

    footer_md = footer_file.read_text(
        encoding="utf-8"
    )

    footer_md = resolve_wiki_links(
        footer_md,
        current_output_file,
        page_index
    )

    return render_footer(
        footer_md,
        logo_for_page(
            SETTINGS,
            current_output_file
        ),
        social_for_page(SETTINGS)
    )


def build_page(
    input_file: Path,
    output_file: Path,
    navigation_raw: list[dict],
    page_index: dict
):

    md_text = input_file.read_text(
        encoding="utf-8"
    )

    md_text = resolve_wiki_links(
        md_text,
        output_file,
        page_index
    )

    blocks = parse_blocks(md_text)

    has_gallery = any(
        block["type"] == "gallery"
        for block in blocks
    )

    body = "\n".join([
        render_block(
            block,
            output_file,
            input_file
        )
        for block in blocks
    ])

    footer = render_footer_for_page(
        output_file,
        page_index
    )

    html = env.get_template(
        "base.html"
    ).render(
        body=body,
        footer=footer,
        navigation=navigation_for_page(
            navigation_raw,
            output_file
        ),
        static_path=static_path_from_output(output_file),
        has_gallery=has_gallery,
        settings=SETTINGS,
        logo=logo_for_page(
            SETTINGS,
            output_file
        )
    )

    output_file.parent.mkdir(
        parents=True,
        exist_ok=True
    )

    output_file.write_text(
        html,
        encoding="utf-8"
    )


def copy_static():

    target = OUTPUT_DIR / "static"

    if target.exists():
        shutil.rmtree(target)

    shutil.copytree(
        STATIC_DIR,
        target
    )


def copy_content_media():

    source = CONTENT_DIR / MEDIA_DIR_NAME
    target = OUTPUT_DIR / MEDIA_DIR_NAME

    if not source.exists():
        source = CONTENT_DIR / LEGACY_MEDIA_DIR_NAME

    if not source.exists():
        return

    if target.exists():
        shutil.rmtree(target)

    shutil.copytree(
        source,
        target
    )


def build_site(format_settings: bool = True):
    global SETTINGS

    run_validation(format_settings=format_settings)
    SETTINGS = load_settings()

    if OUTPUT_DIR.exists():
        shutil.rmtree(OUTPUT_DIR)

    OUTPUT_DIR.mkdir(exist_ok=True)

    copy_static()
    copy_content_media()

    navigation_raw = build_navigation_raw()

    page_index = build_page_index()

    for md_file in CONTENT_DIR.rglob("*.md"):

        if is_footer_file(md_file):
            continue

        if is_settings_file(md_file):
            continue

        output_file = output_path_from_file(
            md_file
        )

        build_page(
            md_file,
            output_file,
            navigation_raw,
            page_index
        )

        print(
            f"gebaut: "
            f"{md_file.relative_to(ROOT_DIR)} "
            f"→ "
            f"{output_file.relative_to(ROOT_DIR)}"
        )


if __name__ == "__main__":
    build_site()
