<?php
/**
 * MCP (Model Context Protocol) Controller
 * Handles JSON-RPC 2.0 requests for pricing information via HTTP endpoint
 * 
 * @package PortalPeople\mcp\controllers
 * @author Portal People Development Team
 */

namespace PortalPeople\mcp\controllers;

use common\models\User;
use Yii;
use yii\web\Controller;
use yii\web\Response;
use PortalPeople\mcp\components\JsonRpc2Validator;
use PortalPeople\mcp\components\PricingService;
use PortalPeople\oauth2\models\OauthClients;

/**
 * McpController provides Model Context Protocol endpoints
 * Exposes pricing tools and resources via JSON-RPC 2.0
 * 
 * Endpoints:
 * - POST /mcp/rpc - Main JSON-RPC 2.0 endpoint
 */
class McpController extends Controller
{
    /**
     * Disable CSRF validation for RPC endpoint
     * @var bool
     */
    public $enableCsrfValidation = false;
    
    /**
     * @var PricingService
     */
    private $pricingService;

    /**
     * {@inheritdoc}
     */
    public function behaviors()
    {
        return [
            [
                'class' => 'yii\filters\ContentNegotiator',
                'formats' => [
                    'application/json' => Response::FORMAT_JSON,
                ],
            ],
            [
                'class' => 'yii\filters\Cors',
                'cors' => [
                    'Origin' => ['*'],
                    'Access-Control-Request-Method' => ['POST', 'OPTIONS'],
                    'Access-Control-Request-Headers' => ['*'],
                    'Access-Control-Allow-Credentials' => false,
                    'Access-Control-Max-Age' => 3600,
                ],
            ],
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function init()
    {
        parent::init();
        $this->pricingService = new PricingService();
    }

    /**
     * Main JSON-RPC 2.0 endpoint
     * Handles all MCP protocol requests
     * 
     * @return array JSON-RPC 2.0 response
     */
    public function actionRpc()
    {
        Yii::$app->response->format = Response::FORMAT_JSON;
        
        $rawBody = Yii::$app->request->getRawBody();
        
        // Log incoming request for debugging
        Yii::info('MCP Request: ' . $this->maskSensitiveJson($rawBody), __METHOD__);
        
        // Validate incoming request
        $validation = JsonRpc2Validator::validateRequest($rawBody);
        
        if (!$validation['valid']) {
            $response = JsonRpc2Validator::createErrorResponse(
                JsonRpc2Validator::ERROR_INVALID_REQUEST,
                'Invalid JSON-RPC 2.0 Request',
                $validation['errors'],
                null
            );
            Yii::warning('Invalid request: ' . json_encode($validation['errors']), __METHOD__);
            return $response;
        }
        
        $request = json_decode($rawBody, true);
        $response = $this->handleRequest($request);
        
        // Validate outgoing response
        $responseValidation = JsonRpc2Validator::validateAndFormat($response, false);
        if ($responseValidation === false) {
            // If our response is invalid, return internal error
            return JsonRpc2Validator::createErrorResponse(
                JsonRpc2Validator::ERROR_INTERNAL,
                'Internal server error - invalid response format',
                null,
                $request['id'] ?? null
            );
        }
        
        // Log successful response
        Yii::info('MCP Response: ' . $this->maskSensitiveJson(json_encode($response)), __METHOD__);
        
        return $response;
    }

    /**
     * Handle JSON-RPC request and route to appropriate method
     * 
     * @param array $request The JSON-RPC request
     * @return array The JSON-RPC response
     */
    protected function handleRequest(array $request): array
    {
        $method = $request['method'] ?? '';
        $params = $request['params'] ?? [];
        $id = $request['id'] ?? null;
        
        try {
            $this->authorizeMcpRequest($method, is_array($params) ? $params : []);

            switch ($method) {
                case 'initialize':
                    return $this->handleInitialize($params, $id);
                    
                case 'tools/list':
                    return $this->listTools($id);
                    
                case 'tools/call':
                    return $this->callTool($params, $id);
                    
                case 'resources/list':
                    return $this->listResources($id);
                    
                case 'resources/read':
                    return $this->readResource($params, $id);
                    
                case 'prompts/list':
                    return $this->listPrompts($id);
                    
                default:
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_METHOD_NOT_FOUND,
                        "Method '{$method}' not found",
                        null,
                        $id
                    );
            }
        } catch (\Exception $e) {
            Yii::error($e->getMessage() . "\n" . $e->getTraceAsString(), __METHOD__);
            return JsonRpc2Validator::createErrorResponse(
                JsonRpc2Validator::ERROR_INTERNAL,
                'Internal error',
                YII_DEBUG ? $e->getMessage() : null,
                $id
            );
        }
    }

    protected function authorizeMcpRequest(string $method, array $params): void
    {
        if (in_array($method, ['initialize', 'notifications/initialized', 'tools/list'], true)) {
            return;
        }

        if ($method === 'tools/call') {
            $toolName = $params['name'] ?? '';
            if (is_string($toolName) && $toolName === 'create_mcp_token') {
                return;
            }

            $arguments = $params['arguments'] ?? [];
            $accessToken = is_array($arguments) ? ($arguments['access_token'] ?? null) : null;
            $this->ensureComposerAuthentication($this->normalizeAccessToken($accessToken));
            return;
        }

        if (in_array($method, ['resources/list', 'resources/read', 'prompts/list'], true)) {
            $this->ensureComposerAuthentication();
        }
    }

    /**
     * Handle MCP initialization
     * 
     * @param array $params
     * @param mixed $id
     * @return array
     */
    protected function handleInitialize(array $params, $id): array
    {
        return JsonRpc2Validator::createSuccessResponse([
            'protocolVersion' => '2024-11-05',
            'capabilities' => [
                'tools' => ['listChanged' => true],
                'resources' => ['subscribe' => true, 'listChanged' => true],
                'prompts' => ['listChanged' => false]
            ],
            'serverInfo' => [
                'name' => 'pricing-server',
                'version' => '1.0.0',
                'description' => 'Portal People Pricing MCP Server'
            ]
        ], $id);
    }

    /**
     * List available tools
     * 
     * @param mixed $id
     * @return array
     */
    protected function listTools($id): array
    {
        $tools = [
            [
                'name' => 'get_price',
                'description' => 'Get pricing information for a specific item/product',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'item_id' => [
                            'type' => 'string',
                            'description' => 'The item/product ID to get pricing for'
                        ],
                        'user_id' => [
                            'type' => 'string',
                            'description' => 'Optional user ID to get user-specific pricing'
                        ],
                        'account_code' => [
                            'type' => 'string',
                            'description' => 'Optional account code to get account-specific pricing'
                        ]
                    ],
                    'required' => ['item_id']
                ]
            ],
            [
                'name' => 'get_prices_bulk',
                'description' => 'Get pricing information for multiple items at once',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'item_ids' => [
                            'type' => 'array',
                            'items' => ['type' => 'string'],
                            'description' => 'Array of item/product IDs'
                        ],
                        'user_id' => [
                            'type' => 'string',
                            'description' => 'Optional user ID for pricing'
                        ]
                    ],
                    'required' => ['item_ids']
                ]
            ],
            [
                'name' => 'search_prices',
                'description' => 'Search for products with pricing based on criteria',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'category' => [
                            'type' => 'string',
                            'description' => 'Filter by product category'
                        ],
                        'min_price' => [
                            'type' => 'number',
                            'description' => 'Minimum price filter'
                        ],
                        'max_price' => [
                            'type' => 'number',
                            'description' => 'Maximum price filter'
                        ],
                        'user_id' => [
                            'type' => 'string',
                            'description' => 'User ID for pricing context'
                        ],
                        'limit' => [
                            'type' => 'integer',
                            'description' => 'Maximum number of results (default: 50)',
                            'default' => 50
                        ]
                    ]
                ]
            ],
            [
                'name' => 'get_account_info',
                'description' => 'Get account information including price band for a user',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'user_id' => [
                            'type' => 'string',
                            'description' => 'User ID to get account info for'
                        ]
                    ],
                    'required' => ['user_id']
                ]
            ],
            [
                'name' => 'create_mcp_token',
                'description' => 'Create an OAuth access token for MCP tools using username/email and password',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'username' => [
                            'type' => 'string',
                            'description' => 'User login identifier (email/username)'
                        ],
                        'password' => [
                            'type' => 'string',
                            'description' => 'User login password'
                        ]
                    ],
                    'required' => ['username', 'password']
                ]
            ],
            [
                'name' => 'composer_update',
                'description' => 'Run composer update for a specific package via ngauge-packagemanager',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'package' => [
                            'type' => 'string',
                            'description' => 'Composer package name in vendor/package format'
                        ],
                        'dry_run' => [
                            'type' => 'boolean',
                            'description' => 'When true, runs composer update in dry-run mode',
                            'default' => false
                        ],
                        'access_token' => [
                            'type' => 'string',
                            'description' => 'OAuth bearer token for composer actions when no authenticated session exists'
                        ]
                    ],
                    'required' => ['package']
                ]
            ],
            [
                'name' => 'composer_update_all',
                'description' => 'Run composer update for all packages via ngauge-packagemanager',
                'inputSchema' => [
                    'type' => 'object',
                    'properties' => [
                        'dry_run' => [
                            'type' => 'boolean',
                            'description' => 'When true, runs composer update in dry-run mode',
                            'default' => false
                        ],
                        'access_token' => [
                            'type' => 'string',
                            'description' => 'OAuth bearer token for composer actions when no authenticated session exists'
                        ]
                    ]
                ]
            ]
        ];
        
        return JsonRpc2Validator::createSuccessResponse(['tools' => $tools], $id);
    }

    /**
     * Execute a tool
     * 
     * @param array $params
     * @param mixed $id
     * @return array
     */
    protected function callTool(array $params, $id): array
    {
        $toolName = $params['name'] ?? '';
        $args = $params['arguments'] ?? [];
        
        switch ($toolName) {
            case 'get_price':
                if (empty($args['item_id'])) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'Missing required parameter: item_id',
                        null,
                        $id
                    );
                }
                
                $result = $this->pricingService->getPrice(
                    $args['item_id'],
                    $args['user_id'] ?? null,
                    $args['account_code'] ?? null
                );
                
                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);
                
            case 'get_prices_bulk':
                if (empty($args['item_ids']) || !is_array($args['item_ids'])) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'Missing or invalid parameter: item_ids (must be array)',
                        null,
                        $id
                    );
                }
                
                $result = $this->pricingService->getPricesBulk(
                    $args['item_ids'],
                    $args['user_id'] ?? null
                );
                
                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);
                
            case 'search_prices':
                $result = $this->pricingService->searchPrices(
                    $args,
                    $args['user_id'] ?? null,
                    $args['limit'] ?? 50
                );
                
                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);
                
            case 'get_account_info':
                if (empty($args['user_id'])) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'Missing required parameter: user_id',
                        null,
                        $id
                    );
                }
                
                $result = $this->pricingService->getAccountInfo($args['user_id']);
                
                if ($result === null) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'User not found or has no account',
                        null,
                        $id
                    );
                }
                
                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);

            case 'create_mcp_token':
                if (empty($args['username']) || !is_string($args['username'])) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'Missing required parameter: username',
                        null,
                        $id
                    );
                }

                if (!array_key_exists('password', $args) || !is_string($args['password']) || trim($args['password']) === '') {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'Missing required parameter: password',
                        null,
                        $id
                    );
                }

                try {
                    $result = $this->createMcpToken($args['username'], $args['password']);
                } catch (\InvalidArgumentException $e) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        $e->getMessage(),
                        null,
                        $id
                    );
                } catch (\Throwable $e) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INTERNAL,
                        $e->getMessage(),
                        null,
                        $id
                    );
                }

                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);

            case 'composer_update':
                if (empty($args['package']) || !is_string($args['package'])) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        'Missing required parameter: package',
                        null,
                        $id
                    );
                }

                $dryRun = $this->parseDryRun($args['dry_run'] ?? false);
                $accessToken = $this->normalizeAccessToken($args['access_token'] ?? null);

                try {
                    $result = $this->runPackageManagerUpdate($args['package'], $dryRun, $accessToken);
                } catch (\InvalidArgumentException $e) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INVALID_PARAMS,
                        $e->getMessage(),
                        null,
                        $id
                    );
                } catch (\Throwable $e) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INTERNAL,
                        $e->getMessage(),
                        null,
                        $id
                    );
                }

                if (!is_array($result) || ($result['success'] ?? false) !== true) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INTERNAL,
                        $result['message'] ?? 'Composer update failed',
                        $result,
                        $id
                    );
                }

                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);

            case 'composer_update_all':
                $dryRun = $this->parseDryRun($args['dry_run'] ?? false);
                $accessToken = $this->normalizeAccessToken($args['access_token'] ?? null);

                try {
                    $result = $this->runPackageManagerUpdateAll($dryRun, $accessToken);
                } catch (\Throwable $e) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INTERNAL,
                        $e->getMessage(),
                        null,
                        $id
                    );
                }

                if (!is_array($result) || ($result['success'] ?? false) !== true) {
                    return JsonRpc2Validator::createErrorResponse(
                        JsonRpc2Validator::ERROR_INTERNAL,
                        $result['message'] ?? 'Composer update all failed',
                        $result,
                        $id
                    );
                }

                return JsonRpc2Validator::createSuccessResponse([
                    'content' => [
                        [
                            'type' => 'text',
                            'text' => json_encode($result, JSON_PRETTY_PRINT)
                        ]
                    ]
                ], $id);
                
            default:
                return JsonRpc2Validator::createErrorResponse(
                    JsonRpc2Validator::ERROR_METHOD_NOT_FOUND,
                    "Tool '{$toolName}' not found",
                    null,
                    $id
                );
        }
    }

    /**
     * List available resources
     * 
     * @param mixed $id
     * @return array
     */
    protected function listResources($id): array
    {
        $resources = [
            [
                'uri' => 'price://item/{item_id}',
                'name' => 'Item Price',
                'description' => 'Price information for a specific item',
                'mimeType' => 'application/json'
            ],
            [
                'uri' => 'price://catalog',
                'name' => 'Price Catalog',
                'description' => 'Complete price catalog',
                'mimeType' => 'application/json'
            ]
        ];
        
        return JsonRpc2Validator::createSuccessResponse(['resources' => $resources], $id);
    }

    /**
     * Read a resource
     * 
     * @param array $params
     * @param mixed $id
     * @return array
     */
    protected function readResource(array $params, $id): array
    {
        $uri = $params['uri'] ?? '';
        
        if (preg_match('#price://item/(.+)#', $uri, $matches)) {
            $itemId = $matches[1];
            $priceData = $this->pricingService->getPrice($itemId);
            
            return JsonRpc2Validator::createSuccessResponse([
                'contents' => [
                    [
                        'uri' => $uri,
                        'mimeType' => 'application/json',
                        'text' => json_encode($priceData, JSON_PRETTY_PRINT)
                    ]
                ]
            ], $id);
        }
        
        return JsonRpc2Validator::createErrorResponse(
            JsonRpc2Validator::ERROR_INVALID_PARAMS,
            "Resource not found: {$uri}",
            null,
            $id
        );
    }

    /**
     * List available prompts
     * 
     * @param mixed $id
     * @return array
     */
    protected function listPrompts($id): array
    {
        return JsonRpc2Validator::createSuccessResponse(['prompts' => []], $id);
    }

    protected function createMcpToken(string $username, string $password): array
    {
        $username = trim($username);

        if ($username === '') {
            throw new \InvalidArgumentException('Username/email cannot be empty.');
        }

        if (trim($password) === '') {
            throw new \InvalidArgumentException('Password cannot be empty.');
        }

        $user = User::findByEmail($username);
        if (!$user instanceof User) {
            $user = User::findByUsername($username);
        }

        if (!$user instanceof User) {
            throw new \RuntimeException('User not found for provided username/email.');
        }

        $oauthModule = $this->getOauth2Module();
        $oauthClient = $this->findOrCreateOauthClient($user);

        $requestParams = [
            'grant_type' => 'password',
            'username' => $user->username,
            'password' => $password,
            'client_id' => $oauthClient->client_id,
        ];

        if (!empty($oauthClient->client_secret)) {
            $requestParams['client_secret'] = $oauthClient->client_secret;
        }

        $oauthRequest = new \OAuth2\Request(
            [],
            $requestParams,
            [],
            [],
            [],
            [
                'REQUEST_METHOD' => 'POST',
                'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
            ]
        );
        $oauthResponse = $oauthModule->getServer()->handleTokenRequest($oauthRequest);
        $tokenData = $oauthResponse->getParameters();

        if ($oauthResponse->getStatusCode() >= 400 || !is_array($tokenData) || empty($tokenData['access_token'])) {
            $message = is_array($tokenData) && !empty($tokenData['error_description'])
                ? $tokenData['error_description']
                : 'Unable to create OAuth token.';
            throw new \RuntimeException($message);
        }

        return [
            'success' => true,
            'token_type' => $tokenData['token_type'] ?? 'Bearer',
            'access_token' => $tokenData['access_token'],
            'refresh_token' => $tokenData['refresh_token'] ?? null,
            'expires_in' => $tokenData['expires_in'] ?? null,
            'scope' => $tokenData['scope'] ?? null,
            'user_id' => $user->user_id,
            'username' => $user->username,
            'email' => $user->email,
            'authorization_header' => 'Bearer ' . $tokenData['access_token'],
        ];
    }

    protected function getOauth2Module(): \yii\base\Module
    {
        $oauthModule = Yii::$app->getModule('oauth2');

        if ($oauthModule === null || !method_exists($oauthModule, 'getServer')) {
            throw new \RuntimeException('OAuth2 module is not configured for MCP token support.');
        }

        return $oauthModule;
    }

    protected function findOrCreateOauthClient(User $user): OauthClients
    {
        $oauthClient = OauthClients::find()
            ->where(['user_id' => $user->user_id])
            ->orderBy(['client_id' => SORT_ASC])
            ->one();

        if ($oauthClient instanceof OauthClients) {
            return $oauthClient;
        }

        $oauthClient = new OauthClients();
        $oauthClient->client_id = str_replace('-', '', (string)$user->id);
        if ($oauthClient->client_id === '') {
            $oauthClient->client_id = bin2hex(random_bytes(16));
        }
        $oauthClient->user_id = (int)$user->user_id;
        $oauthClient->redirect_uri = '/';
        $oauthClient->grant_types = 'client_credentials authorization_code password implicit refresh_token';

        if (!$oauthClient->save()) {
            throw new \RuntimeException('Failed to initialize OAuth client for user.');
        }

        return $oauthClient;
    }

    protected function ensureComposerAuthentication(?string $accessToken = null): void
    {
        if (!Yii::$app->has('user')) {
            throw new \RuntimeException('User component is not configured.');
        }

        if (!Yii::$app->user->isGuest) {
            return;
        }

        $token = $this->normalizeAccessToken($accessToken);
        if ($token === null) {
            $token = $this->extractBearerToken();
        }

        if ($token === null) {
            throw new \RuntimeException('Authentication required. Provide access_token or Authorization Bearer header.');
        }

        $user = $this->resolveUserFromAccessToken($token);

        if (!Yii::$app->user->login($user, 0)) {
            throw new \RuntimeException('Unable to authenticate user from access token.');
        }
    }

    protected function resolveUserFromAccessToken(string $accessToken): User
    {
        $oauthModule = $this->getOauth2Module();
        $oauthRequest = new \OAuth2\Request(
            [],
            ['access_token' => $accessToken],
            [],
            [],
            [],
            ['HTTP_AUTHORIZATION' => 'Bearer ' . $accessToken]
        );
        $oauthResponse = new \OAuth2\Response();
        $server = $oauthModule->getServer();

        if (!$server->verifyResourceRequest($oauthRequest, $oauthResponse)) {
            $responseParams = $oauthResponse->getParameters();
            $message = is_array($responseParams) && !empty($responseParams['error_description'])
                ? $responseParams['error_description']
                : ($oauthResponse->getStatusText() ?: 'Invalid OAuth access token.');
            throw new \RuntimeException($message);
        }

        $tokenData = $server->getAccessTokenData($oauthRequest);
        if (!is_array($tokenData) || empty($tokenData['user_id'])) {
            throw new \RuntimeException('OAuth token does not contain a valid user.');
        }

        $user = User::find()->where(['user_id' => $tokenData['user_id']])->one();
        if (!$user instanceof User) {
            throw new \RuntimeException('OAuth token user was not found.');
        }

        return $user;
    }

    protected function extractBearerToken(): ?string
    {
        $authorization = Yii::$app->request->headers->get('Authorization');

        if (!is_string($authorization) || stripos($authorization, 'Bearer ') !== 0) {
            return null;
        }

        return $this->normalizeAccessToken($authorization);
    }

    protected function normalizeAccessToken($value): ?string
    {
        if (!is_string($value)) {
            return null;
        }

        $token = trim($value);
        if ($token === '') {
            return null;
        }

        if (stripos($token, 'Bearer ') === 0) {
            $token = trim(substr($token, 7));
        }

        return $token !== '' ? $token : null;
    }

    protected function maskSensitiveJson(?string $json): string
    {
        if (!is_string($json) || trim($json) === '') {
            return '';
        }

        $decoded = json_decode($json, true);
        if (!is_array($decoded)) {
            return $json;
        }

        return json_encode($this->maskSensitiveArray($decoded));
    }

    protected function maskSensitiveArray(array $data): array
    {
        $sensitiveKeys = ['password', 'client_secret', 'access_token', 'refresh_token', 'authorization'];

        foreach ($data as $key => $value) {
            if (is_array($value)) {
                $data[$key] = $this->maskSensitiveArray($value);
                continue;
            }

            if (is_string($key) && in_array(strtolower($key), $sensitiveKeys, true)) {
                $data[$key] = '***';
            }
        }

        return $data;
    }

    protected function runPackageManagerUpdate(string $packageName, bool $dryRun = false, ?string $accessToken = null): array
    {
        $packageName = trim($packageName);

        if (!preg_match('/^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/i', $packageName)) {
            throw new \InvalidArgumentException('Invalid package format. Use vendor/package.');
        }

        $this->ensureComposerAuthentication($accessToken);

        $request = Yii::$app->request;
        $originalBodyParams = $request->getBodyParams();
        $packageArg = $dryRun ? $packageName . ' --dry-run' : $packageName;

        try {
            $request->setBodyParams(['package' => $packageArg]);
            $result = Yii::$app->runAction('packagemanager/package-manager/update');
        } finally {
            $request->setBodyParams(is_array($originalBodyParams) ? $originalBodyParams : []);
        }

        return is_array($result) ? $result : [
            'success' => false,
            'message' => 'Unexpected response from package manager update action.',
            'output' => $result,
        ];
    }

    protected function runPackageManagerUpdateAll(bool $dryRun = false, ?string $accessToken = null): array
    {
        $this->ensureComposerAuthentication($accessToken);

        $request = Yii::$app->request;
        $originalBodyParams = $request->getBodyParams();
        $packageArg = $dryRun ? '--dry-run' : '--with-all-dependencies';

        try {
            $request->setBodyParams(['package' => $packageArg]);
            $result = Yii::$app->runAction('packagemanager/package-manager/update');
        } finally {
            $request->setBodyParams(is_array($originalBodyParams) ? $originalBodyParams : []);
        }

        return is_array($result) ? $result : [
            'success' => false,
            'message' => 'Unexpected response from package manager update action.',
            'output' => $result,
        ];
    }

    protected function parseDryRun($value): bool
    {
        $parsed = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
        return $parsed ?? false;
    }
}
