from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from enum import Enum
import uuid
import pickle
import os
from observer import Observable

# ==============================================================================
# 1. FINITE STATE MACHINE (FSM) IMPLEMENTATION
# ==============================================================================

class TransactionContext:
    """
    Context untuk FSM Transaksi.
    Objek ini akan mengubah perilakunya berdasarkan state saat ini.
    """
    def __init__(self, member, item):
        self.transaksi_id = "TRX-" + str(uuid.uuid4())[:8].upper()
        self.member = member
        self.item = item
        self.tgl_pinjam = datetime.now()
        self.tgl_kembali_rencana = self.tgl_pinjam + timedelta(days=14)
        self.denda = 0.0
        self._state = ActiveState()

    def set_state(self, state):
        self._state = state

    def get_status_name(self):
        return self._state.get_name()

    def hitung_denda_saat_ini(self):
        return self._state.calculate_fine(self)

    def proses_pengembalian(self):
        self._state.return_item(self)

    def to_dict(self):
        return {
            "id": self.transaksi_id,
            "item": self.item.judul,
            "nama_member": self.member.nama,
            "tgl_pinjam": self.tgl_pinjam.strftime('%Y-%m-%d'),
            "tgl_kembali": self.tgl_kembali_rencana.strftime('%Y-%m-%d'),
            "status": self.get_status_name(),
            "denda": f"Rp{self.hitung_denda_saat_ini():,.2f}"
        }

class TransactionState(ABC):
    @abstractmethod
    def get_name(self): pass
    @abstractmethod
    def calculate_fine(self, context): pass
    @abstractmethod
    def return_item(self, context): pass

class ActiveState(TransactionState):
    def get_name(self): return "Aktif"
    def calculate_fine(self, context):
        if datetime.now() > context.tgl_kembali_rencana:
            print(f"INFO FSM: Transisi State {context.transaksi_id} [Active -> Overdue]")
            context.set_state(OverdueState())
            return context.hitung_denda_saat_ini()
        return 0.0
    def return_item(self, context):
        print(f"INFO FSM: Transisi State {context.transaksi_id} [Active -> Completed]")
        context.item.ubah_status(StatusItem.TERSEDIA)
        context.set_state(CompletedState())

class OverdueState(TransactionState):
    def get_name(self): return "Terlambat"
    def calculate_fine(self, context):
        selisih_hari = (datetime.now() - context.tgl_kembali_rencana).days
        return float(max(selisih_hari, 0) * 1000)
    def return_item(self, context):
        final_denda = self.calculate_fine(context)
        context.denda = final_denda
        print(f"INFO FSM: Transisi State {context.transaksi_id} [Overdue -> Completed] dengan denda {final_denda}")
        context.item.ubah_status(StatusItem.TERSEDIA)
        context.set_state(CompletedState())

class CompletedState(TransactionState):
    def get_name(self): return "Selesai"
    def calculate_fine(self, context): return context.denda
    def return_item(self, context): raise Exception("Item sudah dikembalikan sebelumnya.")

class StatusItem(Enum):
    TERSEDIA = "Tersedia"
    DIPINJAM = "Dipinjam"

class LoanableItem(ABC):
    def __init__(self, item_id, judul):
        self.item_id = item_id
        self.judul = judul
        self.status = StatusItem.TERSEDIA
    @abstractmethod
    def get_details(self): pass
    def ubah_status(self, status: StatusItem): self.status = status

class Buku(LoanableItem):
    def __init__(self, item_id, judul, pengarang):
        super().__init__(item_id, judul)
        self.pengarang = pengarang
    def get_details(self): return {"Tipe": "Buku", "Pengarang": self.pengarang}

class Majalah(LoanableItem):
    def __init__(self, item_id, judul, edisi):
        super().__init__(item_id, judul)
        self.edisi = edisi
    def get_details(self): return {"Tipe": "Majalah", "Edisi": self.edisi}

class ItemFactory:
    @staticmethod
    def create_item(tipe, item_id, judul, **kwargs):
        if tipe.lower() == 'buku': return Buku(item_id, judul, kwargs.get('pengarang', '-'))
        elif tipe.lower() == 'majalah': return Majalah(item_id, judul, kwargs.get('edisi', '-'))
        raise ValueError("Tipe item tidak dikenal")

class Member:
    def __init__(self, nama, alamat):
        self.member_id = "MEM-" + str(uuid.uuid4())[:8].upper()
        self.nama = nama
        self.alamat = alamat

# --- OBSERVER PATTERN ---

class Perpustakaan(Observable): # Warisi Observable
    def __init__(self, filepath="perpus_data.pkl"):
        super().__init__() # Init Observable
        self.filepath = filepath
        self.items = []
        self.members = []
        self.transactions = []
        self.load_data()

    def add_item(self, item):
        self.items.append(item)
        # Notify View: Item bertambah
        self.notify_observers(event="items_changed", data=self.items)

    def add_member(self, member):
        self.members.append(member)
        # Notify View: Member bertambah
        self.notify_observers(event="members_changed", data=self.members)

    def create_transaction(self, member_id, item_id):
        member = next((m for m in self.members if m.member_id == member_id), None)
        item = next((i for i in self.items if i.item_id == item_id), None)
        
        if not member or not item:
            raise ValueError("Member atau Item tidak ditemukan")
        if item.status != StatusItem.TERSEDIA:
            raise ValueError("Item sedang dipinjam")
            
        trx = TransactionContext(member, item)
        self.transactions.append(trx)
        item.ubah_status(StatusItem.DIPINJAM)
        
        # Notify View: Transaksi baru & status item berubah
        self.notify_observers(event="transactions_changed", data=self.transactions)
        self.notify_observers(event="items_changed", data=self.items) 
        return trx

    def return_transaction(self, trx_id):
        trx = next((t for t in self.transactions if t.transaksi_id == trx_id), None)
        if not trx:
            raise ValueError("Transaksi tidak ditemukan")
        
        trx.proses_pengembalian()
        
        # Notify View: Transaksi update & status item berubah
        self.notify_observers(event="transactions_changed", data=self.transactions)
        self.notify_observers(event="items_changed", data=self.items)
        return trx
    
    def refresh_data(self):
        """Method manual untuk memicu notifikasi semua data (misal saat startup)."""
        self.notify_observers(event="members_changed", data=self.members)
        self.notify_observers(event="items_changed", data=self.items)
        
        # Pastikan hitung denda terbaru sebelum notify
        for t in self.transactions:
            t.hitung_denda_saat_ini()
        self.notify_observers(event="transactions_changed", data=self.transactions)

    def save_data(self):
        data = {"items": self.items, "members": self.members, "transactions": self.transactions}
        with open(self.filepath, 'wb') as f: pickle.dump(data, f)

    def load_data(self):
        if os.path.exists(self.filepath):
            try:
                with open(self.filepath, 'rb') as f:
                    data = pickle.load(f)
                    self.items = data.get("items", [])
                    self.members = data.get("members", [])
                    self.transactions = data.get("transactions", [])
            except Exception as e: print(f"Error loading data: {e}")