from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from enum import Enum
import uuid
import pickle
import os

# ==============================================================================
# Definisi Enum untuk Status
# ==============================================================================

class StatusItem(Enum):
    """Enum untuk status item di perpustakaan."""
    TERSEDIA = "Tersedia"
    DIPINJAM = "Dipinjam"
    HILANG = "Hilang"

class StatusTransaksi(Enum):
    """Enum untuk status transaksi peminjaman."""
    AKTIF = "Aktif"
    SELESAI = "Selesai"
    TERLAMBAT = "Terlambat"

# ==============================================================================
# Hierarki Kelas Item Pustaka (LoanableItem, Buku, Majalah)
# ==============================================================================

class LoanableItem(ABC):
    """
    Kelas abstrak untuk semua item yang dapat dipinjam di perpustakaan.
    Ini adalah superclass (induk) untuk Buku, Majalah, dll.
    """
    def __init__(self, item_id: str, judul: str):
        self._item_id = item_id
        self._judul = judul
        self._status = StatusItem.TERSEDIA

    @abstractmethod
    def get_info(self) -> dict:
        """Metode abstrak untuk mendapatkan informasi detail dari item."""
        pass

    def ubah_status(self, status_baru: StatusItem):
        """Mengubah status ketersediaan item."""
        self._status = status_baru
        print(f"INFO: Status '{self._judul}' telah diubah menjadi {status_baru.value}.")

    @property
    def item_id(self):
        return self._item_id

    @property
    def judul(self):
        return self._judul

    @property
    def status(self):
        return self._status

class Buku(LoanableItem):
    """Kelas turunan untuk merepresentasikan sebuah buku."""
    def __init__(self, item_id: str, judul: str, pengarang: str):
        super().__init__(item_id, judul)
        self._pengarang = pengarang

    def get_info(self) -> dict:
        """Implementasi spesifik untuk menampilkan info buku (method overriding)."""
        return {
            "ID": self._item_id,
            "Judul": self._judul,
            "Tipe": "Buku",
            "Pengarang": self._pengarang,
            "Status": self._status.value
        }

class Majalah(LoanableItem):
    """Kelas turunan untuk merepresentasikan sebuah majalah/jurnal."""
    def __init__(self, item_id: str, judul: str, edisi: str):
        super().__init__(item_id, judul)
        self._edisi = edisi

    def get_info(self) -> dict:
        """Implementasi spesifik untuk menampilkan info majalah (method overriding)."""
        return {
            "ID": self._item_id,
            "Judul": self._judul,
            "Tipe": "Majalah",
            "Edisi": self._edisi,
            "Status": self._status.value
        }

# ==============================================================================
# Kelas-kelas Utama Sistem
# ==============================================================================

class Member:
    """Merepresentasikan anggota perpustakaan."""
    def __init__(self, nama: str, alamat: str):
        self._member_id = "MEM-" + str(uuid.uuid4())[:8].upper()
        self._nama = nama
        self._alamat = alamat

    @property
    def member_id(self):
        return self._member_id
    
    @property
    def nama(self):
        return self._nama

    def get_riwayat_peminjaman(self, perpus: 'Perpustakaan') -> list['Transaksi']:
        """Mendapatkan daftar transaksi yang pernah dilakukan oleh member."""
        riwayat = []
        for transaksi in perpus.daftar_transaksi:
            if transaksi.member.member_id == self._member_id:
                riwayat.append(transaksi)
        return riwayat

class Transaksi:
    """Merepresentasikan sebuah transaksi peminjaman."""
    def __init__(self, member: Member, item: LoanableItem):
        self._transaksi_id = "TRX-" + str(uuid.uuid4())[:8].upper()
        self._member = member
        self._item = item
        self._tgl_pinjam = datetime.now()
        self._tgl_kembali = self._tgl_pinjam + timedelta(days=14) # Batas peminjaman 14 hari
        self._status = StatusTransaksi.AKTIF
        self._denda = 0.0

    @property
    def transaksi_id(self):
        return self._transaksi_id

    @property
    def member(self):
        return self._member
        
    @property
    def item(self):
        return self._item

    @property
    def status(self):
        return self._status

    def hitung_denda(self) -> float:
        """Menghitung denda jika pengembalian terlambat."""
        if datetime.now() > self._tgl_kembali:
            terlambat = (datetime.now() - self._tgl_kembali).days
            self._denda = float(terlambat * 1000) # Denda 1000 per hari
            self._status = StatusTransaksi.TERLAMBAT
        return self._denda
        
    def selesaikan_transaksi(self):
        self.hitung_denda()
        if self._status != StatusTransaksi.TERLAMBAT:
            self._status = StatusTransaksi.SELESAI

class Perpustakaan:
    """Kelas utama yang mengelola seluruh sistem perpustakaan."""
    
    # Kode sebelumnya:

    # def __init__(self):
    #    self._daftar_item = []
    #    self._daftar_member = []
    #    self._daftar_transaksi = []
    #    print("Sistem Perpustakaan berhasil dibuat.")

    def __init__(self, nama_file_data="perpustakaan.pkl"):
        """
        Inisialisasi perpustakaan.
        Akan mencoba memuat data dari file jika ada.
        """
        self._nama_file_data = nama_file_data

        # Coba muat data dulu
        if not self.muat_data():
            # Jika gagal (file tidak ada), buat list kosong
            print("INFO: File data tidak ditemukan. Membuat database baru...")
            self._daftar_item = []
            self._daftar_member = []
            self._daftar_transaksi = []
        
        print("Sistem Perpustakaan berhasil dibuat.")

    @property
    def daftar_item(self):
        return self._daftar_item
        
    @property
    def daftar_member(self):
        return self._daftar_member
        
    @property
    def daftar_transaksi(self):
        return self._daftar_transaksi

    def muat_data(self) -> bool:
        """
        Memuat state perpustakaan (daftar item, member, transaksi)
        dari file pickle.
        Mengembalikan True jika berhasil, False jika gagal.
        """
        if not os.path.exists(self._nama_file_data):
            return False # File tidak ada, gagal memuat
        
        try:
            # Buka file dalam mode Read Binary ('rb')
            with open(self._nama_file_data, 'rb') as f:
                # Muat data yang disimpan
                data_dimuat = pickle.load(f)
                
                # Kembalikan state objek dari dictionary
                self._daftar_item = data_dimuat.get("items", [])
                self._daftar_member = data_dimuat.get("members", [])
                self._daftar_transaksi = data_dimuat.get("transaksi", [])
                
                print(f"INFO: Data perpustakaan berhasil dimuat dari {self._nama_file_data}.")
                return True
        except Exception as e:
            print(f"ERROR saat memuat data: {e}")
            return False
    
    def simpan_data(self):
        """
        Menyimpan state perpustakaan (daftar item, member, transaksi)
        ke dalam file pickle.
        """
        # Siapkan data yang akan disimpan dalam format dictionary
        data_untuk_disimpan = {
            "items": self._daftar_item,
            "members": self._daftar_member,
            "transaksi": self._daftar_transaksi
        }
        
        try:
            # Buka file dalam mode Write Binary ('wb')
            with open(self._nama_file_data, 'wb') as f:
                # Simpan data menggunakan pickle
                pickle.dump(data_untuk_disimpan, f)
            print(f"\nINFO: Data perpustakaan berhasil disimpan ke {self._nama_file_data}.")
        except Exception as e:
            print(f"ERROR saat menyimpan data: {e}")
    
    def tambah_item(self, item: LoanableItem):
        """Menambahkan item baru (buku/majalah) ke perpustakaan."""
        self._daftar_item.append(item)
        print(f"ITEM DITAMBAHKAN: '{item.judul}' ({item.__class__.__name__}) telah ditambahkan ke koleksi.")

    def tambah_member(self, nama: str, alamat: str) -> Member:
        """Mendaftarkan anggota baru."""
        member_baru = Member(nama, alamat)
        self._daftar_member.append(member_baru)
        print(f"MEMBER BARU: {nama} dengan ID {member_baru.member_id} berhasil terdaftar.")
        return member_baru

    def cari_item(self, item_id: str) -> LoanableItem | None:
        """Mencari item berdasarkan ID."""
        for item in self._daftar_item:
            if item.item_id == item_id:
                return item
        return None

    def cari_member(self, member_id: str) -> Member | None:
        """Mencari member berdasarkan ID."""
        for member in self._daftar_member:
            if member.member_id == member_id:
                return member
        return None
        
    def cari_transaksi(self, transaksi_id: str) -> Transaksi | None:
        """Mencari transaksi berdasarkan ID."""
        for trx in self._daftar_transaksi:
            if trx.transaksi_id == transaksi_id:
                return trx
        return None

    def pinjamkan_item(self, member_id: str, item_id: str) -> Transaksi:
        """Memproses peminjaman item oleh member."""
        member = self.cari_member(member_id)
        item = self.cari_item(item_id)

        if not member:
            raise Exception("ERROR: Member tidak ditemukan.")
        if not item:
            raise Exception("ERROR: Item tidak ditemukan.")
        if item.status != StatusItem.TERSEDIA:
            raise Exception(f"ERROR: Item '{item.judul}' sedang tidak tersedia.")

        transaksi_baru = Transaksi(member, item)
        self._daftar_transaksi.append(transaksi_baru)
        item.ubah_status(StatusItem.DIPINJAM)
        print(f"TRANSAKSI PEMINJAMAN: '{item.judul}' dipinjam oleh {member.nama}. ID Transaksi: {transaksi_baru.transaksi_id}")
        return transaksi_baru

    def terima_pengembalian(self, transaksi_id: str):
        """Memproses pengembalian item."""
        transaksi = self.cari_transaksi(transaksi_id)
        if not transaksi:
            raise Exception("ERROR: Transaksi tidak ditemukan.")
        if transaksi.status != StatusTransaksi.AKTIF:
            raise Exception("ERROR: Transaksi ini sudah tidak aktif.")
        
        transaksi.selesaikan_transaksi()
        item = transaksi.item
        item.ubah_status(StatusItem.TERSEDIA)
        
        denda = transaksi.hitung_denda()
        print(f"TRANSAKSI PENGEMBALIAN: '{item.judul}' telah dikembalikan oleh {transaksi.member.nama}.")
        if denda > 0:
            print(f"   -> Dikenakan denda sebesar: Rp{denda:,.2f}")
        else:
            print("   -> Pengembalian tepat waktu.")

# Modifikasi Kelas Pustakawan
class Pustakawan:
    """Merepresentasikan pustakawan yang mengelola perpustakaan."""
    def __init__(self, pustakawan_id: str, nama: str):
        self._pustakawan_id = pustakawan_id
        self._nama = nama

    def lakukan_pendaftaran(self, perpus: Perpustakaan, nama: str, alamat: str) -> Member:
        """Wrapper method untuk mendaftarkan member."""
        print(f"\n[Aksi Pustakawan: {self._nama}] Mendaftarkan anggota baru...")
        return perpus.tambah_member(nama, alamat)

    # Versi Baru dengan Factory
    def tambah_item_ke_koleksi(self, perpus: Perpustakaan, item_type: str, item_id: str, judul: str, **kwargs): 
        """Wrapper method untuk menambah item baru menggunakan Factory."""
        print(f"\n[Aksi Pustakawan: {self._nama}] Meminta pembuatan dan penambahan item '{judul}'...")
        try:
            # Panggil Factory untuk membuat objek
            item_baru = ItemFactory.create_item(item_type, item_id, judul, **kwargs)
            # Tambahkan objek yang sudah jadi ke perpustakaan
            perpus.tambah_item(item_baru)
        except ValueError as e:
            print(f"ERROR saat menambah item: {e}")
        except Exception as e:
            print(f"ERROR tidak terduga: {e}")

    def lakukan_peminjaman(self, perpus: Perpustakaan, member_id: str, item_id: str) -> Transaksi:
        """Wrapper method untuk melakukan peminjaman."""
        print(f"\n[Aksi Pustakawan: {self._nama}] Memproses peminjaman...")
        return perpus.pinjamkan_item(member_id, item_id)
        
    def lakukan_pengembalian(self, perpus: Perpustakaan, transaksi_id: str):
        """Wrapper method untuk melakukan pengembalian."""
        print(f"\n[Aksi Pustakawan: {self._nama}] Memproses pengembalian...")
        perpus.terima_pengembalian(transaksi_id)

# Kelas Baru: ItemFactory
class ItemFactory:
    """Kelas Factory untuk membuat instance LoanableItem."""

    @staticmethod
    def create_item(item_type: str, item_id: str, judul: str, **kwargs) -> LoanableItem:
        """
        Membuat objek Buku atau Majalah berdasarkan item_type.
        kwargs bisa berisi 'pengarang' untuk Buku atau 'edisi' untuk Majalah.
        """
        if item_type.lower() == "buku":
            pengarang = kwargs.get('pengarang', 'N/A') # Ambil pengarang, default N/A
            if pengarang == 'N/A':
                 print("WARNING: Pengarang tidak disediakan untuk buku.")
            return Buku(item_id, judul, pengarang)
        elif item_type.lower() == "majalah":
            edisi = kwargs.get('edisi', 'N/A') # Ambil edisi, default N/A
            if edisi == 'N/A':
                 print("WARNING: Edisi tidak disediakan untuk majalah.")
            return Majalah(item_id, judul, edisi)
        # Bisa tambahkan tipe lain di sini (misal: Artikel)
        # elif item_type.lower() == "artikel":
        #     volume = kwargs.get('volume', 0)
        #     return Artikel(item_id, judul, volume)
        else:
            raise ValueError(f"Tipe item tidak dikenal: {item_type}")

# ==============================================================================
# CONTOH PENGGUNAAN PROGRAM (MAIN)
# ==============================================================================
if __name__ == "__main__":
    print("===== MEMULAI SIMULASI SISTEM PERPUSTAKAAN =====\n")
    
    # 1. Inisialisasi sistem (Otomatis MUAT DATA jika file ada)

    nama_file_db = "perpus_data.pkl"
    perpustakaan_utama = Perpustakaan(nama_file_db)

    # 2. Menambahkan pustakawan
    pustakawan_andi = Pustakawan("P-001", "Andi")

    member_budi = None
    member_citra = None
    transaksi1 = None
    transaksi2 = None
    
    # 3. Pustakawan mendaftarkan member baru

    if len(perpustakaan_utama.daftar_member) == 0:
        print("\n--- Menjalankan setup data baru ---")
        member_budi = pustakawan_andi.lakukan_pendaftaran(perpustakaan_utama, "Budi Santoso", "Jl. Merdeka No. 10")
        member_citra = pustakawan_andi.lakukan_pendaftaran(perpustakaan_utama, "Citra Lestari", "Jl. Pahlawan No. 5")
        
        # 4. Pustakawan menambahkan item baru
        pustakawan_andi.tambah_item_ke_koleksi(perpustakaan_utama, "Buku", "BK-001", "OOP Python", pengarang="Dr. Inovatif")
        pustakawan_andi.tambah_item_ke_koleksi(perpustakaan_utama, "Majalah", "MGZ-001", "Quanta", edisi="Okt 2025")
        pustakawan_andi.tambah_item_ke_koleksi(perpustakaan_utama, "Buku", "BK-002", "Sains Data", pengarang="Prof. Analitika")
    else:
        print("--- Data sudah dimuat dari file ---")
        if len(perpustakaan_utama.daftar_member) > 0:
            member_budi = perpustakaan_utama.daftar_member[0] 
        if len(perpustakaan_utama.daftar_member) > 1:
            member_citra = perpustakaan_utama.daftar_member[1]
    
    if not member_budi or not member_citra:
        print("ERROR: Gagal menginisialisasi data member. Simulasi dihentikan.")
        exit()

    print("\n===== STATUS KOLEKSI DAN MEMBER SAAT INI =====\n")
    print("--- Daftar Item ---")
    # Contoh polimorfisme saat menampilkan info
    for item in perpustakaan_utama.daftar_item:
        print(item.get_info())
    
    print("\n--- Daftar Member ---")
    for member in perpustakaan_utama.daftar_member:
        print(f"ID: {member.member_id}, Nama: {member.nama}")
    
    # 5. Simulasi Transaksi Peminjaman
    print("\n===== SIMULASI TRANSAKSI =====")
    try:
        trx_budi_aktif = next((trx for trx in perpustakaan_utama.daftar_transaksi if trx.member.member_id == member_budi.member_id and trx.item.item_id == "BK-001" and trx.status == StatusTransaksi.AKTIF), None)
        if trx_budi_aktif:
            print(f"INFO: Budi sudah meminjam 'OOP Python' (ID: {trx_budi_aktif.transaksi_id}).")
            transaksi1 = trx_budi_aktif
        else:
            # Budi meminjam buku OOP
            transaksi1 = pustakawan_andi.lakukan_peminjaman(perpustakaan_utama, member_budi.member_id, "BK-001")
        
        trx_citra_aktif = next((trx for trx in perpustakaan_utama.daftar_transaksi if trx.member.member_id == member_citra.member_id and trx.item.item_id == "MGZ-001" and trx.status == StatusTransaksi.AKTIF), None)
        if trx_citra_aktif:
            print(f"INFO: Citra sudah meminjam 'Quanta' (ID: {trx_citra_aktif.transaksi_id}).")
            transaksi2 = trx_citra_aktif
        else:
            # Citra meminjam majalah sains
            transaksi2 = pustakawan_andi.lakukan_peminjaman(perpustakaan_utama, member_citra.member_id, "MGZ-001")
        
        # Budi mencoba meminjam buku yang sama lagi (akan gagal)
        pustakawan_andi.lakukan_peminjaman(perpustakaan_utama, member_budi.member_id, "BK-001")

    except Exception as e:
        print(e)
        
    print("\n===== STATUS SETELAH PEMINJAMAN =====\n")
    for item in perpustakaan_utama.daftar_item:
        print(f"Judul: {item.judul}, Status: {item.status.value}")

    # 6. Simulasi Transaksi Pengembalian
    try:
        if transaksi1 and (transaksi1.status == StatusTransaksi.AKTIF or transaksi1.status == StatusTransaksi.TERLAMBAT):
            # Budi mengembalikan buku OOP
            pustakawan_andi.lakukan_pengembalian(perpustakaan_utama, transaksi1.transaksi_id)
        else:
            print(f"\nINFO: Transaksi {transaksi1.transaksi_id if transaksi1 else 'Budi'} (OOP Python) sudah dikembalikan sebelumnya.")
        
        if transaksi2 and (transaksi2.status == StatusTransaksi.AKTIF or transaksi2.status == StatusTransaksi.TERLAMBAT):
            # Citra mencoba mengembalikan majalah (simulasi terlambat dengan "memajukan" waktu)
            transaksi2._tgl_kembali = datetime.now() - timedelta(days=3) # Anggap terlambat 3 hari
            pustakawan_andi.lakukan_pengembalian(perpustakaan_utama, transaksi2.transaksi_id)
        else:
            print(f"\nINFO: Transaksi {transaksi2.transaksi_id if transaksi2 else 'Citra'} (Quanta) sudah dikembalikan sebelumnya.")

    except Exception as e:
        print(f"ERROR SIMULASI PENGEMBALIAN: {e}")
        
    print("\n===== STATUS AKHIR SETELAH PENGEMBALIAN =====\n")
    for item in perpustakaan_utama.daftar_item:
        print(f"Judul: {item.judul}, Status: {item.status.value}")
        
    print("\n===== RIWAYAT PEMINJAMAN BUDI =====\n")
    riwayat_budi = member_budi.get_riwayat_peminjaman(perpustakaan_utama)
    if riwayat_budi:
        for trx in riwayat_budi:
            print(f"ID: {trx.transaksi_id}, Item: '{trx.item.judul}', Status: {trx.status.value}")
    else:
        print("Tidak ada riwayat peminjaman untuk Budi.")

    print("\n===== RIWAYAT PEMINJAMAN CITRA =====\n")
    riwayat_citra = member_citra.get_riwayat_peminjaman(perpustakaan_utama)
    if riwayat_citra:
        for trx in riwayat_citra:
            print(f"ID: {trx.transaksi_id}, Item: '{trx.item.judul}', Status: {trx.status.value}")
    else:
        print("Tidak ada riwayat peminjaman untuk Citra.")

    # 7. Simpan state terakhir sebelum program selesai
    try:
        perpustakaan_utama.simpan_data()
    except Exception as e:
        print(f"Gagal menyimpan data di akhir sesi: {e}")
        
    print("\n===== SIMULASI SELESAI =====")