"""filetool: CLI pengelola file sederhana."""

import argparse
import fnmatch
import json
import logging
import os
import shutil
import sys
from datetime import datetime
from pathlib import Path

VERSI = "1.0.0"

KONFIG_BAWAAN = {
    "pola_eksklusi": [".git", "__pycache__", "*.tmp", "*~"],
    "kedalaman_rekursif": 5,
    "gunakan_sampah": False,
    "folder_sampah": ".filetool-sampah",
}


def format_size(ukuran: int) -> str:
    """Ubah ukuran dalam byte menjadi teks yang mudah dibaca manusia."""
    if ukuran < 1024:
        return f"{ukuran} B"
    for satuan in ("KB", "MB", "GB", "TB"):
        ukuran /= 1024
        if ukuran < 1024:
            return f"{ukuran:.1f} {satuan}".replace(".", ",")
    return f"{ukuran:.1f} PB".replace(".", ",")


def format_waktu(epoch: float) -> str:
    """Ubah stempel waktu epoch menjadi teks tanggal dan jam."""
    return datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M")


def parse_exclude(nilai: str | list[str] | None) -> list[str]:
    """Normalisasi pola eksklusi dari string terpisah koma atau list."""
    if not nilai:
        return []
    kandidat = nilai.split(",") if isinstance(nilai, str) else list(nilai)
    return [pola.strip() for pola in kandidat if pola.strip()]


def cocok_pola(nama: str, pola: list[str]) -> bool:
    """Cek apakah sebuah nama cocok dengan pola fnmatch mana pun."""
    return any(fnmatch.fnmatch(nama, satu_pola) for satu_pola in pola)


def filter_files(daftar: list[Path], pola: list[str]) -> list[Path]:
    """Saring path yang namanya cocok pola eksklusi mana pun."""
    return [p for p in daftar if not cocok_pola(p.name, pola)]


def muat_konfig() -> dict | None:
    """Muat konfigurasi berlapis: nilai bawaan, lalu berkas JSON bila ada."""
    config = dict(KONFIG_BAWAAN)
    jalur = os.environ.get("FILETOOL_CONFIG", "config.json")
    berkas = Path(jalur)
    if not berkas.exists():
        if os.environ.get("FILETOOL_CONFIG"):
            print(f"filetool: konfigurasi tidak ditemukan: {berkas}", file=sys.stderr)
            return None
        logging.debug("tidak ada berkas konfigurasi; memakai nilai bawaan")
        return config
    try:
        with berkas.open(encoding="utf-8") as f:
            data = json.load(f)
    except PermissionError:
        print(f"filetool: izin ditolak saat membaca: {berkas}", file=sys.stderr)
        return None
    except json.JSONDecodeError as e:
        print(f"filetool: konfigurasi bukan JSON valid: {berkas} "
              f"(baris {e.lineno}, kolom {e.colno})", file=sys.stderr)
        return None
    if not isinstance(data, dict):
        print(f"filetool: akar konfigurasi harus objek: {berkas}", file=sys.stderr)
        return None
    config.update(data)
    logging.debug("konfigurasi dimuat dari %s", berkas)
    return config


def telusuri(akar: Path, config: dict) -> tuple[list[Path], list[Path]]:
    """Kumpulkan (berkas, direktori anak) secara rekursif, hormati eksklusi."""
    hasil_berkas: list[Path] = []
    hasil_direktori: list[Path] = []
    pola = parse_exclude(config.get("pola_eksklusi"))
    kedalaman = config.get("kedalaman_rekursif", 5)

    def jalan(direktori: Path, tingkat: int) -> None:
        if tingkat > kedalaman:
            logging.debug("berhenti sebelum %s (batas kedalaman %d)", direktori, kedalaman)
            return
        for entri in sorted(direktori.iterdir()):
            if cocok_pola(entri.name, pola):
                logging.debug("lewati %s (cocok pola eksklusi)", entri)
                continue
            if entri.is_dir():
                hasil_direktori.append(entri)
                jalan(entri, tingkat + 1)
            else:
                hasil_berkas.append(entri)

    jalan(akar, 1)
    return hasil_berkas, hasil_direktori


def perintah_daftar(args: argparse.Namespace, config: dict) -> int:
    direktori = Path(args.path)
    for entri in sorted(direktori.iterdir()):
        penanda = "d" if entri.is_dir() else "-"
        if args.panjang:
            stat = entri.stat()
            ukuran = "-" if entri.is_dir() else format_size(stat.st_size)
            print(f"{penanda} {ukuran:>8}  {format_waktu(stat.st_mtime)}  {entri.name}")
        else:
            akhiran = "/" if entri.is_dir() else ""
            print(f"{entri.name}{akhiran}")
    return 0


def perintah_info(args: argparse.Namespace, config: dict) -> int:
    berkas = Path(args.path)
    stat = berkas.stat()
    jenis = "direktori" if berkas.is_dir() else "berkas"
    baris = [
        ("jalur", str(berkas)),
        ("jenis", jenis),
    ]
    if berkas.is_dir():
        baris.append(("isi", f"{len(list(berkas.iterdir()))} entri"))
    else:
        baris.append(("ekstensi", berkas.suffix or "(tidak ada)"))
        baris.append(("ukuran", f"{stat.st_size} byte ({format_size(stat.st_size)})"))
    baris.append(("diubah", format_waktu(stat.st_mtime)))
    for label, nilai in baris:
        print(f"{label:<9}: {nilai}")
    return 0


def perintah_cari(args: argparse.Namespace, config: dict) -> int:
    akar = Path(args.path)
    pola = parse_exclude(config.get("pola_eksklusi")) + parse_exclude(args.kecuali)
    berkas_semua, _ = telusuri(akar, {**config, "pola_eksklusi": pola})
    hasil = sorted(b for b in berkas_semua if fnmatch.fnmatch(b.name, args.pola))
    if not hasil:
        print("tidak ada yang cocok.")
        return 0
    for jalur in hasil:
        print(jalur.relative_to(akar))
    return 0


def perintah_salin(args: argparse.Namespace, config: dict) -> int:
    sumber = Path(args.sumber)
    tujuan = Path(args.tujuan)
    if sumber.is_dir():
        print(f"filetool: sumber harus berkas, bukan direktori: {sumber}", file=sys.stderr)
        return 1
    if tujuan.is_dir():
        tujuan = tujuan / sumber.name
    if tujuan.exists():
        print(f"filetool: tujuan sudah ada: {tujuan}", file=sys.stderr)
        return 1
    shutil.copy2(sumber, tujuan)
    print(f"tersalin: {sumber} -> {tujuan}")
    return 0


def perintah_pindahkan(args: argparse.Namespace, config: dict) -> int:
    sumber = Path(args.sumber)
    tujuan = Path(args.tujuan)
    if tujuan.is_dir():
        tujuan = tujuan / sumber.name
    if tujuan.exists():
        print(f"filetool: tujuan sudah ada: {tujuan}", file=sys.stderr)
        return 1
    shutil.move(sumber, tujuan)
    print(f"dipindahkan: {sumber} -> {tujuan}")
    return 0


def perintah_hapus(args: argparse.Namespace, config: dict) -> int:
    target = Path(args.path)
    if not target.is_file():
        print(f"filetool: bukan berkas: {target}", file=sys.stderr)
        return 1
    if not args.ya:
        print(f"menolak menghapus tanpa --ya: {target}")
        print("jalankan ulang dengan --ya untuk benar-benar menghapus.")
        return 1
    if config.get("gunakan_sampah"):
        folder_sampah = Path(config.get("folder_sampah", ".filetool-sampah"))
        folder_sampah.mkdir(exist_ok=True)
        tujuan = folder_sampah / target.name
        if tujuan.exists():
            print(f"filetool: sudah ada di sampah: {tujuan}", file=sys.stderr)
            return 1
        shutil.move(target, tujuan)
        print(f"ke sampah: {target} -> {tujuan}")
        return 0
    target.unlink()
    print(f"terhapus: {target}")
    return 0


def perintah_ukuran(args: argparse.Namespace, config: dict) -> int:
    akar = Path(args.path)
    daftar_berkas, daftar_direktori = telusuri(akar, config)
    total = sum(b.stat().st_size for b in daftar_berkas)
    mentah = f"{total:,}".replace(",", ".")
    print(f"{'total':<9}: {mentah} byte ({format_size(total)})")
    print(f"{'berkas':<9}: {len(daftar_berkas)}")
    print(f"{'direktori':<9}: {len(daftar_direktori)}")
    return 0


def susun_rencana(akar: Path, pola: list[str]) -> list[tuple[Path, Path]]:
    """Rancang pemindahan setiap berkas ke folder sesuai ekstensinya."""
    rencana = []
    for berkas in filter_files(sorted(akar.iterdir()), pola):
        if not berkas.is_file():
            continue
        folder = berkas.suffix.lstrip(".") or "tanpa-ekstensi"
        rencana.append((berkas, akar / folder / berkas.name))
    return rencana


def perintah_organisasi(args: argparse.Namespace, config: dict) -> int:
    akar = Path(args.path)
    pola = parse_exclude(config.get("pola_eksklusi")) + parse_exclude(args.kecuali)
    rencana = susun_rencana(akar, pola)
    if not rencana:
        print("tidak ada berkas yang perlu diorganisasi.")
        return 0
    for sumber, tujuan in rencana:
        print(f"{sumber} -> {tujuan}")
    if args.dry_run:
        print(f"(dry-run: {len(rencana)} berkas akan dipindahkan)")
        return 0
    for sumber, tujuan in rencana:
        tujuan.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(sumber, tujuan)
    print(f"{len(rencana)} berkas dipindahkan.")
    return 0


def buat_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="filetool",
        description="Pengelola file dari terminal.",
    )
    parser.add_argument("--versi", action="version", version=f"filetool {VERSI}")
    parser.add_argument("--verbose", action="store_true",
                        help="tampilkan pesan debug yang lebih rinci")
    sub = parser.add_subparsers(dest="perintah", required=True, metavar="PERINTAH")

    p_daftar = sub.add_parser("daftar", help="tampilkan isi sebuah direktori")
    p_daftar.add_argument("path", help="direktori yang ingin ditampilkan")
    p_daftar.add_argument("--panjang", action="store_true",
                          help="tampilkan ukuran, waktu ubah, dan nama")
    p_daftar.set_defaults(func=perintah_daftar)

    p_info = sub.add_parser("info", help="tampilkan metadata sebuah berkas")
    p_info.add_argument("path", help="berkas atau direktori yang ingin diperiksa")
    p_info.set_defaults(func=perintah_info)

    p_cari = sub.add_parser("cari", help="cari berkas berdasarkan pola glob")
    p_cari.add_argument("path", help="direktori awal pencarian")
    p_cari.add_argument("--pola", default="*", help='pola nama berkas, misalnya "*.txt"')
    p_cari.add_argument("--kecuali", default="",
                        help="pola tambahan yang diabaikan, dipisah koma")
    p_cari.set_defaults(func=perintah_cari)

    p_salin = sub.add_parser("salin", help="salin sebuah berkas")
    p_salin.add_argument("sumber", help="berkas sumber")
    p_salin.add_argument("tujuan", help="berkas atau direktori tujuan")
    p_salin.set_defaults(func=perintah_salin)

    p_pindahkan = sub.add_parser("pindahkan", help="pindahkan berkas atau direktori")
    p_pindahkan.add_argument("sumber", help="berkas atau direktori sumber")
    p_pindahkan.add_argument("tujuan", help="berkas atau direktori tujuan")
    p_pindahkan.set_defaults(func=perintah_pindahkan)

    p_hapus = sub.add_parser("hapus", help="hapus sebuah berkas")
    p_hapus.add_argument("path", help="berkas yang akan dihapus")
    p_hapus.add_argument("--ya", action="store_true",
                         help="konfirmasi bahwa pengguna yakin ingin menghapus")
    p_hapus.set_defaults(func=perintah_hapus)

    p_ukuran = sub.add_parser("ukuran", help="hitung total ukuran secara rekursif")
    p_ukuran.add_argument("path", help="direktori yang ingin diukur")
    p_ukuran.set_defaults(func=perintah_ukuran)

    p_organisasi = sub.add_parser("organisasi", help="urutkan berkas ke folder per ekstensi")
    p_organisasi.add_argument("path", help="direktori yang akan diorganisasi")
    p_organisasi.add_argument("--dry-run", action="store_true",
                              help="tampilkan rencana tanpa memindahkan apa pun")
    p_organisasi.add_argument("--kecuali", default="",
                              help="pola tambahan yang diabaikan, dipisah koma")
    p_organisasi.set_defaults(func=perintah_organisasi)

    return parser


def main(argv: list[str] | None = None) -> int:
    args = buat_parser().parse_args(argv)
    logging.basicConfig(
        level=logging.DEBUG if args.verbose else logging.WARNING,
        format="[%(levelname)s] %(message)s",
    )
    config = muat_konfig()
    if config is None:
        return 1
    try:
        return args.func(args, config)
    except BrokenPipeError:
        # Pembaca pipa (misalnya `head`) menutup keluaran lebih awal; bukan galat.
        os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
        return 0
    except FileNotFoundError as e:
        print(f"filetool: tidak ditemukan: {e.filename or e}", file=sys.stderr)
        return 1
    except NotADirectoryError as e:
        print(f"filetool: bukan direktori: {e.filename or e}", file=sys.stderr)
        return 1
    except PermissionError as e:
        print(f"filetool: izin ditolak: {e.filename or e}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    sys.exit(main())
