#!/usr/bin/env python3
"""OtterWiki → MediaWiki migration script."""

import argparse
import re
import subprocess
from pathlib import Path

import requests

REPO = Path('/mnt/ssd/dbs/otter/app-data/repository')
API = 'http://localhost:8099/api.php'

_FN_DEF = re.compile(r'^\[(\^[^\]]+)\]:\s*(.*)')

# Cached pandoc availability (None = not yet checked)
_PANDOC_AVAILABLE: bool | None = None


def _cap(s: str) -> str:
    """Title-case: capitalize first letter of each word."""
    return s.title() if s else s


def _pandoc_available() -> bool:
    global _PANDOC_AVAILABLE
    if _PANDOC_AVAILABLE is None:
        try:
            _PANDOC_AVAILABLE = subprocess.run(
                ['pandoc', '--version'], capture_output=True
            ).returncode == 0
        except FileNotFoundError:
            _PANDOC_AVAILABLE = False
    return _PANDOC_AVAILABLE


# ---------------------------------------------------------------------------
# MediaWiki session
# ---------------------------------------------------------------------------

def mw_login(user: str, password: str):
    s = requests.Session()
    r = s.get(API, params={'action': 'query', 'meta': 'tokens', 'type': 'login', 'format': 'json'})
    token = r.json()['query']['tokens']['logintoken']
    s.post(API, data={'action': 'login', 'lgname': user, 'lgpassword': password,
                      'lgtoken': token, 'format': 'json'})
    r = s.get(API, params={'action': 'query', 'meta': 'tokens', 'format': 'json'})
    csrf = r.json()['query']['tokens']['csrftoken']
    return s, csrf


# ---------------------------------------------------------------------------
# Title determination
# ---------------------------------------------------------------------------

def page_title(md_path: Path) -> str:
    parts = md_path.relative_to(REPO).parts
    if md_path.name == 'home.md' and len(parts) == 1:
        return 'Заглавная страница'
    return _cap(md_path.stem)


# ---------------------------------------------------------------------------
# Markdown → wikitext conversion
# ---------------------------------------------------------------------------

def convert_pandoc(text: str) -> str:
    return subprocess.run(
        ['pandoc', '-f', 'markdown', '-t', 'mediawiki'],
        input=text, capture_output=True, text=True
    ).stdout


def convert_python(text: str, skip_info: bool = False) -> str:
    lines = text.split('\n')

    # Collect footnote definitions in one pass
    footnotes: dict[str, str] = {}
    for line in lines:
        m = _FN_DEF.match(line)
        if m:
            footnotes[m.group(1)] = m.group(2)

    def replace_fn(m):
        key = m.group(0)[1:-1]  # strip outer [ ] to match footnotes dict keys
        content = footnotes.get(key, m.group(0))
        content = re.sub(r'\[([^\]^][^\]]*)\]\(([^)]+)\)', r'[\2 \1]', content)
        return f'<ref>{content}</ref>'

    out = []
    for line in lines:
        if _FN_DEF.match(line):
            continue

        m = re.match(r'^(#{1,6})\s+(.*)', line)
        if m:
            eq = '=' * len(m.group(1))
            out.append(f'{eq} {m.group(2)} {eq}')
            continue

        if re.match(r'^---+$', line.strip()):
            out.append('----')
            continue

        line = re.sub(r'^(\s*)-(\s)', r'\1*\2', line)
        line = re.sub(r'\*\*\*(.+?)\*\*\*', r"'''''\1'''''", line)
        line = re.sub(r'\*\*(.+?)\*\*', r"'''\1'''", line)
        line = re.sub(r'\*(.+?)\*', r"''\1''", line)
        line = re.sub(r'(?<!!)\[([^\]^!][^\]]*)\]\(([^)]+)\)', r'[\2 \1]', line)
        line = re.sub(r'\[\^\S+?\]', replace_fn, line)
        out.append(line)

    result = convert_tables('\n'.join(out), skip_info=skip_info)
    if footnotes:
        result += '\n<references />'
    return result


def _split_cells(line: str) -> list[str]:
    """Split a markdown table row on | but not inside [[ ]]."""
    cells = []
    depth = 0
    current = []
    i = 0
    # Strip leading/trailing |
    line = line.strip()
    if line.startswith('|'):
        line = line[1:]
    if line.endswith('|'):
        line = line[:-1]
    while i < len(line):
        if line[i:i+2] == '[[':
            depth += 1
            current.append('[[')
            i += 2
        elif line[i:i+2] == ']]':
            depth -= 1
            current.append(']]')
            i += 2
        elif line[i] == '|' and depth == 0:
            cells.append(''.join(current).strip())
            current = []
            i += 1
        else:
            current.append(line[i])
            i += 1
    cells.append(''.join(current).strip())
    return cells


def convert_tables(text: str, skip_info: bool = False) -> str:
    lines = text.split('\n')
    out = []
    in_table = False
    skip_table = False

    for line in lines:
        if re.match(r'^\|', line):
            cells = _split_cells(line)
            if all(re.match(r'^:?-+:?$', c) for c in cells if c):
                # Separator row: start or continue table
                if not in_table:
                    header_line = out.pop() if out else ''
                    hcells = _split_cells(header_line)
                    # Blank-header table (all-empty header cells) = OtterWiki info table
                    if skip_info and all(c == '' for c in hcells):
                        skip_table = True
                        in_table = True
                    else:
                        out += ['{| class="wikitable"', '|-', '! ' + ' !! '.join(hcells)]
                        in_table = True
                        skip_table = False
                if not skip_table:
                    out.append('|-')
            else:
                if in_table:
                    if not skip_table:
                        out.append('|-')
                        out.append('| ' + ' || '.join(cells))
                    # else: skip info table row
                else:
                    out.append(line)
        else:
            if in_table:
                if not skip_table:
                    out.append('|}')
                in_table = False
                skip_table = False
            out.append(line)

    if in_table and not skip_table:
        out.append('|}')

    return '\n'.join(out)


_PERSON_FIELD_MAP = {
    'родился': 'родился', 'родилась': 'родился',
    'умер': 'умер', 'умерла': 'умер',
    'отец': 'отец', 'мать': 'мать',
    'супруг': 'супруг', 'супруга': 'супруг', 'муж': 'супруг', 'жена': 'супруг',
    'дети': 'дети', 'ребёнок': 'дети',
    'братья': 'братья', 'брат': 'братья', 'сестра': 'братья',
    'сёстры': 'братья', 'сестры': 'братья',
    'место рождения': 'место_рождения', 'место_рождения': 'место_рождения',
    'прочее': 'прочее',
}
_PERSONA_PARAM_ORDER = ['родился', 'место_рождения', 'умер', 'отец', 'мать', 'супруг', 'дети', 'братья', 'прочее']

_PLACE_FIELD_MAP = {
    'тип': 'тип',
    'статус': 'статус',
    'страна': 'страна',
    'регион': 'регион', 'область': 'регион',
    'район': 'район', 'расположение': 'район', 'самоуправление': 'район',
    'река': 'река',
    'основана': 'основана', 'основан': 'основана',
    'население': 'население',
    'адрес': 'адрес',
    'период': 'период', 'годы': 'период',
    'жильцы': 'жильцы', 'жильцы/семья': 'жильцы', 'семейное имя': 'прочее',
    'латв. название': 'назв_латыш',
    'белор. название': 'назв_белор',
    'координаты': 'координаты',
    'сайт': 'сайт',
    'телефон': 'телефон', 'email': 'телефон',
    'полное название': 'прочее', 'классы': 'прочее',
    'штаб-квартира': 'прочее', 'сотрудников': 'прочее',
}
_PLACE_PARAM_ORDER = ['тип', 'статус', 'страна', 'регион', 'район', 'река', 'основана',
                      'население', 'адрес', 'период', 'жильцы', 'назв_латыш', 'назв_белор',
                      'координаты', 'сайт', 'телефон', 'прочее']


def _extract_infobox(text: str, title: str, name_param: str, template: str,
                     field_map: dict, param_order: list) -> tuple[str, str]:
    """Extract photo + blank-header info table; return ({{Template|...}}, cleaned_text)."""
    lines = text.split('\n')
    photo = None
    fields: dict[str, str] = {}
    remove: set[int] = set()

    for i, line in enumerate(lines):
        if re.match(r'^\|', line):
            break  # reached info table — stop looking for photo
        m = re.match(r'^\s*\[!\[[^\]]*\]\(\./(?:[^/)]+/)?([^)?]+?)(?:\?[^)]*)?\)\]', line)
        if m:
            photo = m.group(1)
            remove.add(i)
            break

    in_table = False
    for i, line in enumerate(lines):
        if re.match(r'^\|', line):
            cells = _split_cells(line)
            if all(re.match(r'^:?-+:?$', c) for c in cells if c):
                if not in_table:
                    prev = i - 1
                    if prev >= 0 and re.match(r'^\|', lines[prev]):
                        hcells = _split_cells(lines[prev])
                        if all(c == '' for c in hcells):
                            in_table = True
                            remove.add(prev)
                if in_table:
                    remove.add(i)
            elif in_table:
                remove.add(i)
                if len(cells) >= 2:
                    key = re.sub(r'\*\*(.+?)\*\*', r'\1', cells[0]).strip().lower()
                    val = re.sub(r'\*\*(.+?)\*\*', r"'''\1'''", cells[1].strip())
                    param = field_map.get(key)
                    if param and param not in fields:
                        fields[param] = val
        elif in_table:
            in_table = False

    if not photo and not fields:
        return '', text

    parts = ['{{' + template, f'| {name_param:<16} = {title}']
    if photo:
        parts.append(f'| фото            = {photo}')
    for param in param_order:
        if param in fields:
            parts.append(f'| {param:<16} = {fields[param]}')
    infobox = '\n'.join(parts) + '\n}}'
    cleaned = '\n'.join(line for i, line in enumerate(lines) if i not in remove)
    return infobox, cleaned


def extract_person_infobox(text: str, title: str) -> tuple[str, str]:
    return _extract_infobox(text, title, 'имя', 'Персона', _PERSON_FIELD_MAP, _PERSONA_PARAM_ORDER)


def extract_place_infobox(text: str, title: str) -> tuple[str, str]:
    return _extract_infobox(text, title, 'название', 'Место', _PLACE_FIELD_MAP, _PLACE_PARAM_ORDER)


def strip_first_heading(text: str) -> str:
    """Remove the first H1 line — MW displays the page title itself."""
    return re.sub(r'^#[^#][^\n]*\n?', '', text, count=1)


def convert(text: str, skip_info: bool = False, is_place: bool = False, title: str = '') -> str:
    text = strip_first_heading(text)
    infobox = ''
    if skip_info:
        infobox, text = extract_person_infobox(text, title)
    elif is_place:
        infobox, text = extract_place_infobox(text, title)
    result = convert_pandoc(text) if _pandoc_available() else convert_python(text, skip_info=skip_info)
    if infobox:
        result = infobox + '\n' + result.lstrip('\n')
    return result


# ---------------------------------------------------------------------------
# Post-processing
# ---------------------------------------------------------------------------

def fix_links(text: str) -> str:
    pattern = (r'\[\[([^\]|]+)\|'
               r'(?:Люди|Места|Воспоминания|люди|места|воспоминания|Место)'
               r'/([^\]]+)\]\]')

    def replace_link(m):
        display = m.group(1).strip()
        page = _cap(m.group(2).strip().lower())
        if display.lower() == page.lower():
            return f'[[{page}]]'
        return f'[[{page}|{display}]]'

    text = re.sub(pattern, replace_link, text)
    # Also handle bare section paths: [[Section/PageName]] → [[PageName]]
    text = re.sub(
        r'\[\[(?:Люди|Места|Воспоминания|люди|места|воспоминания|Место)/([^\]|]+)\]\]',
        lambda m: f'[[{m.group(1).strip().lower().title()}]]',
        text
    )
    return text


def fix_images(text: str) -> str:
    # Handle linked images: [![](./file.jpg?thumbnail=400)](./file.jpg)
    # and plain images:     ![alt](./file.jpg?thumbnail=400)
    pattern = r'(?:\[)?!\[[^\]]*\]\(\./(?:[^/)]+/)?([^)?]+?)(\?[^)]*)?\)(?:\]\([^)]*\))?'

    def replace_img(m):
        filename = m.group(1)
        size_m = re.search(r'thumbnail=(\d+)', m.group(2) or '')
        return f'[[File:{filename}|{size_m.group(1)}px]]' if size_m else f'[[File:{filename}]]'

    return re.sub(pattern, replace_img, text)


_CATEGORY_MAP = {'люди': 'Люди', 'места': 'Места', 'воспоминания': 'Воспоминания'}


def category_suffix(md_path: Path) -> str:
    parts = md_path.relative_to(REPO).parts
    if len(parts) == 1:
        return '' if md_path.name == 'home.md' else '\n\n[[Category:Статьи]]'
    cat = _CATEGORY_MAP.get(parts[0].lower())
    return f'\n\n[[Category:{cat}]]' if cat else ''


# ---------------------------------------------------------------------------
# MW operations
# ---------------------------------------------------------------------------

def post_page(session, csrf: str, title: str, text: str, dry_run: bool) -> bool:
    if dry_run:
        print(f'[DRY] {title}')
        return True
    data = session.post(API, data={
        'action': 'edit', 'title': title, 'text': text,
        'token': csrf, 'format': 'json'
    }).json()
    if 'error' in data:
        print(f'[ERR] {title}: {data["error"].get("info", data["error"])}')
        return False
    print(f'[OK] {title}')
    return True


def upload_image(session, csrf: str, image_path: Path, dry_run: bool) -> bool:
    basename = image_path.name
    if dry_run:
        print(f'[DRY] File:{basename}')
        return True
    with open(image_path, 'rb') as f:
        data = session.post(API, data={
            'action': 'upload', 'filename': basename,
            'token': csrf, 'format': 'json', 'ignorewarnings': '1'
        }, files={'file': f}).json()
    if 'error' in data:
        print(f'[ERR] File:{basename}: {data["error"].get("info", data["error"])}')
        return False
    if data.get('upload', {}).get('result') == 'Success':
        print(f'[OK] File:{basename}')
    else:
        print(f'[SKIP] File:{basename} (already exists or no change)')
    return True


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def collect_images() -> list[Path]:
    images = []
    for folder in ('люди', 'места'):
        p = REPO / folder
        if p.exists():
            for ext in ('*.jpg', '*.jpeg', '*.JPG', '*.JPEG', '*.png', '*.PNG'):
                images.extend(p.rglob(ext))
    return images


def main():
    parser = argparse.ArgumentParser(description='Migrate OtterWiki to MediaWiki')
    parser.add_argument('--user', required=True)
    parser.add_argument('--password', required=True)
    parser.add_argument('--dry-run', action='store_true')
    args = parser.parse_args()

    session = csrf = None
    if not args.dry_run:
        session, csrf = mw_login(args.user, args.password)

    pages_ok = pages_err = images_ok = images_err = 0

    for md_path in sorted(REPO.rglob('*.md')):
        title = page_title(md_path)
        raw = md_path.read_text(encoding='utf-8')
        parts = md_path.relative_to(REPO).parts
        is_people = len(parts) > 0 and parts[0].lower() == 'люди'
        is_place = len(parts) > 0 and parts[0].lower() == 'места'
        wikitext = convert(raw, skip_info=is_people, is_place=is_place, title=title)
        wikitext = fix_links(wikitext)
        wikitext = fix_images(wikitext)
        wikitext += category_suffix(md_path)
        if post_page(session, csrf, title, wikitext, args.dry_run):
            pages_ok += 1
        else:
            pages_err += 1

    for img in collect_images():
        if upload_image(session, csrf, img, args.dry_run):
            images_ok += 1
        else:
            images_err += 1

    print(f'\nDone: {pages_ok} pages, {images_ok} images, {pages_err + images_err} errors')


if __name__ == '__main__':
    main()
