// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "./Vault.sol";
import "./tokens/DepositToken.sol";
import "./tokens/DebtToken.sol";
import "./interfaces/ILedger.sol";

/**
 * @title VaultFactory
 * @notice Factory for creating vault instances with associated tokens
 * @dev Creates Vault, DepositToken, and DebtToken instances
 */
contract VaultFactory is AccessControl {
    bytes32 public constant VAULT_DEPLOYER_ROLE = keccak256("VAULT_DEPLOYER_ROLE");

    address public immutable vaultImplementation;
    address public immutable depositTokenImplementation;
    address public immutable debtTokenImplementation;

    ILedger public ledger;
    address public entityRegistry;
    address public collateralAdapter;
    address public eMoneyJoin;
    address public gruVaultIndex;

    mapping(address => address[]) public vaultsByEntity; // entity => vaults[]
    mapping(address => address) public vaultToEntity; // vault => entity

    event VaultCreated(
        address indexed vault,
        address indexed entity,
        address indexed owner,
        address depositToken,
        address debtToken
    );

    constructor(
        address admin,
        address vaultImplementation_,
        address depositTokenImplementation_,
        address debtTokenImplementation_,
        address ledger_,
        address entityRegistry_,
        address collateralAdapter_,
        address eMoneyJoin_
    ) {
        _grantRole(DEFAULT_ADMIN_ROLE, admin);
        _grantRole(VAULT_DEPLOYER_ROLE, admin);

        vaultImplementation = vaultImplementation_;
        depositTokenImplementation = depositTokenImplementation_;
        debtTokenImplementation = debtTokenImplementation_;
        ledger = ILedger(ledger_);
        entityRegistry = entityRegistry_;
        collateralAdapter = collateralAdapter_;
        eMoneyJoin = eMoneyJoin_;
    }

    /**
     * @notice Wire GRU vault index (optional). Grants FACTORY_ROLE on index to this factory.
     */
    function setGruVaultIndex(address index) external onlyRole(DEFAULT_ADMIN_ROLE) {
        gruVaultIndex = index;
    }

    function _recordGruVault(
        address vault,
        address entity,
        address baseToken,
        address depositToken,
        address debtToken,
        uint8 gruTier,
        bytes32 ibanHash,
        bytes32 policyProfileKey
    ) internal {
        if (gruVaultIndex == address(0)) return;
        (bool ok, bytes memory data) = gruVaultIndex.call(
            abi.encodeWithSignature(
                "recordVault(address,address,address,address,address,uint8,bytes32,bytes32)",
                vault,
                entity,
                baseToken,
                depositToken,
                debtToken,
                gruTier,
                ibanHash,
                policyProfileKey
            )
        );
        ok;
        data;
    }

    /**
     * @notice Create a new vault for a regulated entity
     * @param owner Vault owner address
     * @param entity Regulated entity address
     * @param asset Collateral asset address (for deposit token)
     * @param currency eMoney currency address (for debt token)
     * @return vault Address of created vault
     * @return depositToken Address of deposit token
     * @return debtToken Address of debt token
     */
    function createVault(
        address owner,
        address entity,
        address asset,
        address currency
    ) external onlyRole(VAULT_DEPLOYER_ROLE) returns (
        address vault,
        address depositToken,
        address debtToken
    ) {
        require(owner != address(0), "VaultFactory: zero owner");
        require(entity != address(0), "VaultFactory: zero entity");

        // Deploy vault directly (not using proxy for simplicity)
        // In production, could use proxy pattern for upgradeability
        Vault vaultContract = new Vault(
            owner,
            entity,
            address(ledger),
            entityRegistry,
            collateralAdapter,
            eMoneyJoin
        );
        vault = address(vaultContract);

        // Deploy deposit token (factory as admin so it can grant MINTER/BURNER to vault)
        bytes memory depositTokenInitData = abi.encodeWithSelector(
            DepositToken.initialize.selector,
            string(abi.encodePacked("Deposit ", _getAssetSymbol(asset))),
            string(abi.encodePacked("d", _getAssetSymbol(asset))),
            vault,
            asset,
            address(this)
        );

        ERC1967Proxy depositTokenProxy = new ERC1967Proxy(depositTokenImplementation, depositTokenInitData);
        depositToken = address(depositTokenProxy);

        // Grant minter/burner roles to vault
        DepositToken(depositToken).grantRole(keccak256("MINTER_ROLE"), vault);
        DepositToken(depositToken).grantRole(keccak256("BURNER_ROLE"), vault);

        // Deploy debt token (factory as admin so it can grant MINTER/BURNER to vault)
        bytes memory debtTokenInitData = abi.encodeWithSelector(
            DebtToken.initialize.selector,
            string(abi.encodePacked("Debt ", _getCurrencySymbol(currency))),
            string(abi.encodePacked("debt", _getCurrencySymbol(currency))),
            vault,
            currency,
            address(this)
        );

        ERC1967Proxy debtTokenProxy = new ERC1967Proxy(debtTokenImplementation, debtTokenInitData);
        debtToken = address(debtTokenProxy);

        // Grant minter/burner roles to vault
        DebtToken(debtToken).grantRole(keccak256("MINTER_ROLE"), vault);
        DebtToken(debtToken).grantRole(keccak256("BURNER_ROLE"), vault);

        // Configure vault with tokens (cast via payable since Vault has receive())
        Vault(payable(vault)).setDepositToken(asset, depositToken);
        Vault(payable(vault)).setDebtToken(currency, debtToken);

        // Grant vault role in ledger
        ledger.registerVault(vault);

        // Track vault
        vaultsByEntity[entity].push(vault);
        vaultToEntity[vault] = entity;

        emit VaultCreated(vault, entity, owner, depositToken, debtToken);
    }

    /**
     * @notice Create a vault with explicit decimals and debt transferability (DEX-ready)
     * @param depositDecimals Deposit token decimals (e.g. 6 for stablecoins; 0 = 18)
     * @param debtDecimals Debt token decimals (0 = 18)
     * @param debtTransferable If true, debt token is freely transferable (DEX-ready)
     */
    function createVaultWithDecimals(
        address owner,
        address entity,
        address asset,
        address currency,
        uint8 depositDecimals,
        uint8 debtDecimals,
        bool debtTransferable
    ) external onlyRole(VAULT_DEPLOYER_ROLE) returns (
        address vault,
        address depositToken,
        address debtToken
    ) {
        return _deployVaultWithDecimals(
            owner, entity, asset, currency, depositDecimals, debtDecimals, debtTransferable
        );
    }

    function _deployVaultWithDecimals(
        address owner,
        address entity,
        address asset,
        address currency,
        uint8 depositDecimals,
        uint8 debtDecimals,
        bool debtTransferable
    ) internal returns (
        address vault,
        address depositToken,
        address debtToken
    ) {
        require(owner != address(0), "VaultFactory: zero owner");
        require(entity != address(0), "VaultFactory: zero entity");

        Vault vaultContract = new Vault(
            owner,
            entity,
            address(ledger),
            entityRegistry,
            collateralAdapter,
            eMoneyJoin
        );
        vault = address(vaultContract);

        uint8 dDec = depositDecimals > 0 ? depositDecimals : 18;
        bytes memory depositTokenInitData = abi.encodeWithSelector(
            DepositToken.initializeWithDecimals.selector,
            string(abi.encodePacked("Deposit ", _getAssetSymbol(asset))),
            string(abi.encodePacked("d", _getAssetSymbol(asset))),
            vault,
            asset,
            address(this),
            dDec
        );

        ERC1967Proxy depositTokenProxy = new ERC1967Proxy(depositTokenImplementation, depositTokenInitData);
        depositToken = address(depositTokenProxy);

        DepositToken(depositToken).grantRole(keccak256("MINTER_ROLE"), vault);
        DepositToken(depositToken).grantRole(keccak256("BURNER_ROLE"), vault);

        uint8 debtDec = debtDecimals > 0 ? debtDecimals : 18;
        bytes memory debtTokenInitData = abi.encodeWithSelector(
            DebtToken.initializeFull.selector,
            string(abi.encodePacked("Debt ", _getCurrencySymbol(currency))),
            string(abi.encodePacked("debt", _getCurrencySymbol(currency))),
            vault,
            currency,
            address(this),
            debtDec,
            debtTransferable
        );

        ERC1967Proxy debtTokenProxy = new ERC1967Proxy(debtTokenImplementation, debtTokenInitData);
        debtToken = address(debtTokenProxy);

        DebtToken(debtToken).grantRole(keccak256("MINTER_ROLE"), vault);
        DebtToken(debtToken).grantRole(keccak256("BURNER_ROLE"), vault);

        Vault(payable(vault)).setDepositToken(asset, depositToken);
        Vault(payable(vault)).setDebtToken(currency, debtToken);

        ledger.registerVault(vault);

        vaultsByEntity[entity].push(vault);
        vaultToEntity[vault] = entity;

        emit VaultCreated(vault, entity, owner, depositToken, debtToken);
    }

    /**
     * @notice Create vault with explicit GRU tier, IBAN hash, and policy profile key.
     */
    function createVaultWithDecimalsGRU(
        address owner,
        address entity,
        address asset,
        address currency,
        uint8 depositDecimals,
        uint8 debtDecimals,
        bool debtTransferable,
        uint8 gruTier,
        bytes32 ibanHash,
        bytes32 policyProfileKey
    ) external onlyRole(VAULT_DEPLOYER_ROLE) returns (address vault, address depositToken, address debtToken) {
        (vault, depositToken, debtToken) = _deployVaultWithDecimals(
            owner, entity, asset, currency, depositDecimals, debtDecimals, debtTransferable
        );
        _recordGruVault(vault, entity, asset, depositToken, debtToken, gruTier, ibanHash, policyProfileKey);
    }

    /**
     * @notice Get asset symbol (helper)
     * @param asset Asset address
     * @return symbol Asset symbol
     */
    function _getAssetSymbol(address asset) internal pure returns (string memory symbol) {
        if (asset == address(0)) {
            return "ETH";
        }
        // In production, would fetch from ERC20
        return "ASSET";
    }

    /**
     * @notice Get currency symbol (helper)
     * @param currency Currency address
     * @return symbol Currency symbol
     */
    function _getCurrencySymbol(address currency) internal pure returns (string memory symbol) {
        // In production, would fetch from eMoney token
        return "CURRENCY";
    }

    /**
     * @notice Get vaults for an entity
     * @param entity Entity address
     * @return vaults Array of vault addresses
     */
    function getVaultsByEntity(address entity) external view returns (address[] memory vaults) {
        return vaultsByEntity[entity];
    }
}
