#!/usr/bin/env python3
"""
Read-only FTP mirror of the legacy shodh.net file folders that the web crawl
could not reach (unlinked uploads — notably any Vol.15/16 print-copy PDFs).

STRICTLY READ-ONLY: this script only issues directory listings and downloads
(LIST/NLST/RETR). It contains no upload, rename, or delete calls — production
must never be modified (standing rule).

Run from shodh-sanchayan-api/:
    python3 scripts/legacy_ftp_mirror.py
You will be prompted for the FTP username and password (the password is not
echoed and is never written anywhere). Re-running is safe: files already
downloaded with the same size are skipped.

Output: legacy-import/backup/files/<remote path>   (gitignored)
"""

import ftplib
import getpass
import os
import re
import socket
import sys
from pathlib import Path

HOST = "shodh.net"
# Folders to mirror, relative to the FTP root. cPanel FTP usually lands in
# the account home, with the website under public_html/; both layouts are tried.
TARGETS = [
    "phocadownload",            # all article PDFs incl. unlinked ones
    "media",                    # forms, fonts, misc uploads
    "ugc_pdf",                  # UGC documents
    "images",                   # site images (covers, banners)
    "templates/shodh/images",   # theme artwork (logo/banner originals)
    "shodh-forms",              # registry app source (small; documents the data)
]
OUT = Path(__file__).resolve().parent.parent / "legacy-import" / "backup" / "files"


def connect(user: str, password: str) -> ftplib.FTP:
    """Prefer FTP over TLS; fall back to plain FTP if the server refuses TLS."""
    try:
        ftp = ftplib.FTP_TLS(HOST, timeout=60)
        ftp.login(user, password)
        ftp.prot_p()
        print(f"connected to {HOST} (TLS)")
        return ftp
    except (ftplib.error_perm, ssl_error_types(), socket.error) as e:
        print(f"TLS login not accepted ({e.__class__.__name__}); retrying plain FTP")
    ftp = ftplib.FTP(HOST, timeout=60)
    ftp.login(user, password)
    print(f"connected to {HOST} (plain FTP)")
    return ftp


def ssl_error_types():
    import ssl
    return ssl.SSLError


def list_dir(ftp: ftplib.FTP, path: str):
    """Returns (dirs, files{name: size}) for a remote path using MLSD, falling back to NLST."""
    dirs, files = [], {}
    try:
        for name, facts in ftp.mlsd(path):
            if name in (".", ".."):
                continue
            if facts.get("type") == "dir":
                dirs.append(name)
            elif facts.get("type") == "file":
                files[name] = int(facts.get("size", -1))
    except ftplib.error_perm:
        # Older servers: probe each entry
        for name in ftp.nlst(path):
            base = name.rsplit("/", 1)[-1]
            if base in (".", ".."):
                continue
            try:
                ftp.cwd(f"{path}/{base}")
                ftp.cwd("/")
                dirs.append(base)
            except ftplib.error_perm:
                try:
                    files[base] = ftp.size(f"{path}/{base}") or -1
                except ftplib.error_perm:
                    files[base] = -1
    return dirs, files


def mirror(ftp: ftplib.FTP, remote: str, local: Path, stats: dict):
    dirs, files = list_dir(ftp, remote)
    local.mkdir(parents=True, exist_ok=True)
    for name, size in files.items():
        target = local / name
        if target.exists() and size >= 0 and target.stat().st_size == size:
            stats["skipped"] += 1
            continue
        with open(target, "wb") as fh:
            ftp.retrbinary(f"RETR {remote}/{name}", fh.write)
        stats["downloaded"] += 1
        stats["bytes"] += target.stat().st_size
        print(f"  {remote}/{name}  ({target.stat().st_size // 1024} KB)")
    for d in dirs:
        mirror(ftp, f"{remote}/{d}", local / d, stats)


def main():
    user = input("FTP username: ").strip()
    password = getpass.getpass("FTP password (not shown): ")
    ftp = connect(user, password)

    root_dirs, _ = list_dir(ftp, ".")
    base = "public_html" if "public_html" in root_dirs else "."
    print(f"web root detected as: {base}/  (top-level entries: {', '.join(sorted(root_dirs)[:12])})")

    stats = {"downloaded": 0, "skipped": 0, "bytes": 0, "missing": []}
    for t in TARGETS:
        remote = f"{base}/{t}" if base != "." else t
        try:
            ftp.cwd(remote)
            ftp.cwd("/")
        except ftplib.error_perm:
            stats["missing"].append(t)
            print(f"- {remote}: not present, skipping")
            continue
        print(f"+ mirroring {remote}/")
        mirror(ftp, remote, OUT / t, stats)

    ftp.quit()
    print(f"\nDone. downloaded={stats['downloaded']}  skipped(existing)={stats['skipped']}  "
          f"total={stats['bytes'] / 1048576:.1f} MB  ->  {OUT}")
    if stats["missing"]:
        print("not found on server:", ", ".join(stats["missing"]))

    # The thing we are really looking for. Match on the whole relative path (the
    # volume is usually only in the folder name) and allow every separator the
    # legacy uploads use: "vol-16-issue-1-2", "Vol. 16_", "Vol_16", "vol16".
    vol_re = re.compile(r"vol[\s._-]*1[56]\b", re.IGNORECASE)
    hits = [p for p in OUT.rglob("*") if p.is_file() and vol_re.search(str(p.relative_to(OUT)))]
    print(f"\nVol.15/16 candidates found: {len(hits)}")
    for h in hits:
        print("  ", h.relative_to(OUT))


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        sys.exit("\ninterrupted — re-run to resume; completed files are kept")
