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

import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol";

/**
 * @title ShipmentTracker
 * @dev Smart contract for tracking shipments with manufacturer, buyer, and logistics information
 * @notice This contract is upgradeable and uses role-based access control
 */
contract ShipmentTracker is
    Initializable,
    AccessControlUpgradeable,
    UUPSUpgradeable,
    PausableUpgradeable
{
    // Role definitions
    bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
    bytes32 public constant MANUFACTURER_ROLE = keccak256("MANUFACTURER_ROLE");
    bytes32 public constant BUYER_ROLE = keccak256("BUYER_ROLE");
    bytes32 public constant LOGISTICS_ROLE = keccak256("LOGISTICS_ROLE");
    bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE");

    // Shipment status enum
    enum ShipmentStatus {
        Created,
        ManufacturerConfirmed,
        InTransit,
        Delivered,
        Cancelled
    }

    // Party information structure
    struct PartyInfo {
        address walletAddress;
        string name;
        string companyId;
        uint256 timestamp;
        bytes32 dataHash; // Hash of additional off-chain data
    }

    // Shipment structure
    struct Shipment {
        uint256 shipmentId;
        string externalId; // External reference ID (from Odoo, etc.)
        PartyInfo manufacturer;
        PartyInfo buyer;
        PartyInfo logistics;
        ShipmentStatus status;
        uint256 createdAt;
        uint256 updatedAt;
        string[] documents; // IPFS hashes or URLs of documents
        bytes32 dataHash; // Hash of shipment data for integrity
        bool exists;
    }

    // Storage
    mapping(uint256 => Shipment) private shipments;
    mapping(string => uint256) private externalIdToShipmentId;
    uint256 private shipmentCounter;

    // Events
    event ShipmentCreated(
        uint256 indexed shipmentId,
        string externalId,
        address indexed manufacturer,
        address indexed buyer,
        uint256 timestamp
    );

    event ShipmentUpdated(
        uint256 indexed shipmentId,
        ShipmentStatus status,
        address updatedBy,
        uint256 timestamp
    );

    event ManufacturerInfoUpdated(
        uint256 indexed shipmentId,
        address indexed manufacturer,
        bytes32 dataHash,
        uint256 timestamp
    );

    event BuyerInfoUpdated(
        uint256 indexed shipmentId,
        address indexed buyer,
        bytes32 dataHash,
        uint256 timestamp
    );

    event LogisticsInfoUpdated(
        uint256 indexed shipmentId,
        address indexed logistics,
        bytes32 dataHash,
        uint256 timestamp
    );

    event DocumentAdded(
        uint256 indexed shipmentId,
        string documentHash,
        address addedBy,
        uint256 timestamp
    );

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    /**
     * @dev Initialize the contract
     * @param _admin Admin address
     */
    function initialize(address _admin) public initializer {
        __AccessControl_init();
        __UUPSUpgradeable_init();
        __Pausable_init();

        _grantRole(DEFAULT_ADMIN_ROLE, _admin);
        _grantRole(ADMIN_ROLE, _admin);
        _grantRole(UPGRADER_ROLE, _admin);

        shipmentCounter = 1;
    }

    /**
     * @dev Create a new shipment
     * @param externalId External reference ID
     * @param manufacturerAddress Manufacturer wallet address
     * @param manufacturerName Manufacturer name
     * @param manufacturerCompanyId Manufacturer company ID
     * @param buyerAddress Buyer wallet address
     * @param buyerName Buyer name
     * @param buyerCompanyId Buyer company ID
     * @param dataHash Hash of shipment data
     */
    function createShipment(
        string memory externalId,
        address manufacturerAddress,
        string memory manufacturerName,
        string memory manufacturerCompanyId,
        address buyerAddress,
        string memory buyerName,
        string memory buyerCompanyId,
        bytes32 dataHash
    ) external whenNotPaused returns (uint256) {
        require(
            hasRole(ADMIN_ROLE, msg.sender) ||
            hasRole(MANUFACTURER_ROLE, msg.sender),
            "Not authorized to create shipment"
        );
        require(externalIdToShipmentId[externalId] == 0, "Shipment with this external ID already exists");
        require(manufacturerAddress != address(0), "Invalid manufacturer address");
        require(buyerAddress != address(0), "Invalid buyer address");

        uint256 shipmentId = shipmentCounter++;

        Shipment storage newShipment = shipments[shipmentId];
        newShipment.shipmentId = shipmentId;
        newShipment.externalId = externalId;
        newShipment.status = ShipmentStatus.Created;
        newShipment.createdAt = block.timestamp;
        newShipment.updatedAt = block.timestamp;
        newShipment.dataHash = dataHash;
        newShipment.exists = true;

        // Set manufacturer info
        newShipment.manufacturer = PartyInfo({
            walletAddress: manufacturerAddress,
            name: manufacturerName,
            companyId: manufacturerCompanyId,
            timestamp: block.timestamp,
            dataHash: dataHash
        });

        // Set buyer info
        newShipment.buyer = PartyInfo({
            walletAddress: buyerAddress,
            name: buyerName,
            companyId: buyerCompanyId,
            timestamp: block.timestamp,
            dataHash: dataHash
        });

        // Initialize empty logistics info
        newShipment.logistics = PartyInfo({
            walletAddress: address(0),
            name: "",
            companyId: "",
            timestamp: 0,
            dataHash: bytes32(0)
        });

        externalIdToShipmentId[externalId] = shipmentId;

        emit ShipmentCreated(
            shipmentId,
            externalId,
            manufacturerAddress,
            buyerAddress,
            block.timestamp
        );

        return shipmentId;
    }

    /**
     * @dev Update logistics information for a shipment
     * @param shipmentId Shipment ID
     * @param logisticsAddress Logistics provider wallet address
     * @param logisticsName Logistics provider name
     * @param logisticsCompanyId Logistics provider company ID
     * @param dataHash Hash of logistics data
     */
    function updateLogisticsInfo(
        uint256 shipmentId,
        address logisticsAddress,
        string memory logisticsName,
        string memory logisticsCompanyId,
        bytes32 dataHash
    ) external whenNotPaused {
        require(shipments[shipmentId].exists, "Shipment does not exist");
        require(
            hasRole(ADMIN_ROLE, msg.sender) ||
            hasRole(LOGISTICS_ROLE, msg.sender),
            "Not authorized to update logistics info"
        );
        require(logisticsAddress != address(0), "Invalid logistics address");

        Shipment storage shipment = shipments[shipmentId];
        shipment.logistics = PartyInfo({
            walletAddress: logisticsAddress,
            name: logisticsName,
            companyId: logisticsCompanyId,
            timestamp: block.timestamp,
            dataHash: dataHash
        });
        shipment.updatedAt = block.timestamp;

        emit LogisticsInfoUpdated(shipmentId, logisticsAddress, dataHash, block.timestamp);
    }

    /**
     * @dev Update shipment status
     * @param shipmentId Shipment ID
     * @param newStatus New status
     */
    function updateShipmentStatus(
        uint256 shipmentId,
        ShipmentStatus newStatus
    ) external whenNotPaused {
        require(shipments[shipmentId].exists, "Shipment does not exist");

        Shipment storage shipment = shipments[shipmentId];

        // Check authorization based on status change
        if (newStatus == ShipmentStatus.ManufacturerConfirmed) {
            require(
                hasRole(MANUFACTURER_ROLE, msg.sender) ||
                hasRole(ADMIN_ROLE, msg.sender) ||
                msg.sender == shipment.manufacturer.walletAddress,
                "Not authorized"
            );
        } else if (newStatus == ShipmentStatus.InTransit) {
            require(
                hasRole(LOGISTICS_ROLE, msg.sender) ||
                hasRole(ADMIN_ROLE, msg.sender) ||
                msg.sender == shipment.logistics.walletAddress,
                "Not authorized"
            );
        } else if (newStatus == ShipmentStatus.Delivered) {
            require(
                hasRole(LOGISTICS_ROLE, msg.sender) ||
                hasRole(BUYER_ROLE, msg.sender) ||
                hasRole(ADMIN_ROLE, msg.sender) ||
                msg.sender == shipment.logistics.walletAddress ||
                msg.sender == shipment.buyer.walletAddress,
                "Not authorized"
            );
        } else {
            require(hasRole(ADMIN_ROLE, msg.sender), "Not authorized");
        }

        shipment.status = newStatus;
        shipment.updatedAt = block.timestamp;

        emit ShipmentUpdated(shipmentId, newStatus, msg.sender, block.timestamp);
    }

    /**
     * @dev Add a document to a shipment
     * @param shipmentId Shipment ID
     * @param documentHash IPFS hash or document reference
     */
    function addDocument(
        uint256 shipmentId,
        string memory documentHash
    ) external whenNotPaused {
        require(shipments[shipmentId].exists, "Shipment does not exist");
        require(
            hasRole(ADMIN_ROLE, msg.sender) ||
            hasRole(MANUFACTURER_ROLE, msg.sender) ||
            hasRole(BUYER_ROLE, msg.sender) ||
            hasRole(LOGISTICS_ROLE, msg.sender),
            "Not authorized to add documents"
        );

        Shipment storage shipment = shipments[shipmentId];
        shipment.documents.push(documentHash);
        shipment.updatedAt = block.timestamp;

        emit DocumentAdded(shipmentId, documentHash, msg.sender, block.timestamp);
    }

    /**
     * @dev Get shipment details
     * @param shipmentId Shipment ID
     */
    function getShipment(uint256 shipmentId) external view returns (
        uint256 id,
        string memory externalId,
        ShipmentStatus status,
        PartyInfo memory manufacturer,
        PartyInfo memory buyer,
        PartyInfo memory logistics,
        uint256 createdAt,
        uint256 updatedAt,
        bytes32 dataHash
    ) {
        require(shipments[shipmentId].exists, "Shipment does not exist");
        Shipment storage shipment = shipments[shipmentId];

        return (
            shipment.shipmentId,
            shipment.externalId,
            shipment.status,
            shipment.manufacturer,
            shipment.buyer,
            shipment.logistics,
            shipment.createdAt,
            shipment.updatedAt,
            shipment.dataHash
        );
    }

    /**
     * @dev Get shipment by external ID
     * @param externalId External reference ID
     */
    function getShipmentByExternalId(string memory externalId) external view returns (uint256) {
        uint256 shipmentId = externalIdToShipmentId[externalId];
        require(shipmentId != 0, "Shipment not found");
        return shipmentId;
    }

    /**
     * @dev Get shipment documents
     * @param shipmentId Shipment ID
     */
    function getShipmentDocuments(uint256 shipmentId) external view returns (string[] memory) {
        require(shipments[shipmentId].exists, "Shipment does not exist");
        return shipments[shipmentId].documents;
    }

    /**
     * @dev Pause the contract
     */
    function pause() external onlyRole(ADMIN_ROLE) {
        _pause();
    }

    /**
     * @dev Unpause the contract
     */
    function unpause() external onlyRole(ADMIN_ROLE) {
        _unpause();
    }

    /**
     * @dev Required override for UUPS upgrades
     */
    function _authorizeUpgrade(address newImplementation)
        internal
        onlyRole(UPGRADER_ROLE)
        override
    {}

    /**
     * @dev Get total number of shipments
     */
    function getTotalShipments() external view returns (uint256) {
        return shipmentCounter - 1;
    }
}
