<?php

namespace App\Modules\Xero\Services;

use App\Modules\Xero\Models\XeroToken;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
use Carbon\Carbon;
use Exception;

/**
 * Xero OAuth2 Authentication Service
 * 
 * Handles OAuth2 flow, token management, and authentication
 */
class XeroAuthService
{
    protected $clientId;
    protected $clientSecret;
    protected $redirectUri;
    protected $scopes = [
        'offline_access',
        'accounting.transactions',
        'accounting.contacts',
        'accounting.settings',
        'accounting.attachments',
        'accounting.reports.read'
    ];
    
    public function __construct()
    {
        // Use database settings instead of config/env values
        $this->clientId = \App\Models\Setting::get('integrations.xero.client_id', '');
        $this->clientSecret = \App\Models\Setting::get('integrations.xero.client_secret', '');
        $this->redirectUri = \App\Models\Setting::get('integrations.xero.redirect_uri', url('/api/v1/xero/callback'));
    }
    
    /**
     * Generate OAuth2 authorization URL
     */
    public function getAuthorizationUrl()
    {
        $state = $this->generateState();
        
        $params = [
            'response_type' => 'code',
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUri,
            'scope' => implode(' ', $this->scopes),
            'state' => $state
        ];
        
        // Store state in session for verification
        session(['xero_oauth_state' => $state]);
        
        return 'https://login.xero.com/identity/connect/authorize?' . http_build_query($params);
    }
    
    /**
     * Handle OAuth2 callback and exchange code for tokens
     */
    public function handleCallback($code, $state = null)
    {
        // Verify state if provided
        if ($state && $state !== session('xero_oauth_state')) {
            throw new Exception('Invalid OAuth state parameter');
        }
        
        // Exchange code for access token
        $tokenData = $this->exchangeCodeForToken($code);
        
        // Get connected organizations/tenants
        $tenants = $this->getConnectedTenants($tokenData['access_token']);
        
        if (empty($tenants)) {
            throw new Exception('No Xero organizations found for this connection');
        }
        
        // Store tokens as system-wide tokens (user_id = null)
        $expiresAt = Carbon::now()->addSeconds($tokenData['expires_in']);
        
        foreach ($tenants as $tenant) {
            $token = XeroToken::updateOrCreate(
                [
                    'user_id' => null, // System-wide token
                    'tenant_id' => $tenant['tenantId']
                ],
                [
                    'access_token' => $tokenData['access_token'],
                    'refresh_token' => $tokenData['refresh_token'],
                    'expires_at' => $expiresAt,
                    'tenant_name' => $tenant['tenantName'] ?? null,
                    'tenant_type' => $tenant['tenantType'] ?? null,
                    'connected_at' => Carbon::now()
                ]
            );
        }
        
        // Return the first token for verification
        return XeroToken::whereNull('user_id')
            ->whereNotNull('access_token')
            ->orderBy('connected_at', 'desc')
            ->first();
    }
    
    /**
     * Exchange authorization code for access token
     */
    protected function exchangeCodeForToken($code)
    {
        $response = Http::asForm()->post('https://identity.xero.com/connect/token', [
            'grant_type' => 'authorization_code',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'code' => $code,
            'redirect_uri' => $this->redirectUri
        ]);
        
        if (!$response->successful()) {
            Log::error('Xero token exchange failed', [
                'status' => $response->status(),
                'body' => $response->body()
            ]);
            
            throw new Exception('Failed to exchange code for access token: ' . $response->body());
        }
        
        return $response->json();
    }
    
    /**
     * Get connected organizations/tenants
     */
    protected function getConnectedTenants($accessToken)
    {
        $response = Http::withToken($accessToken)
            ->get('https://api.xero.com/connections');
            
        if (!$response->successful()) {
            Log::error('Failed to get Xero tenants', [
                'status' => $response->status(),
                'body' => $response->body()
            ]);
            
            throw new Exception('Failed to get connected organizations');
        }
        
        return $response->json();
    }
    
    /**
     * Store tokens in database
     */
    public function storeTokens($tokenData, $userId)
    {
        foreach ($tokenData['tenants'] as $tenant) {
            XeroToken::updateOrCreate(
                [
                    'user_id' => $userId,
                    'tenant_id' => $tenant['tenantId']
                ],
                [
                    'access_token' => $tokenData['access_token'],
                    'refresh_token' => $tokenData['refresh_token'],
                    'expires_at' => $tokenData['expires_at'],
                    'tenant_name' => $tenant['tenantName'] ?? null,
                    'tenant_type' => $tenant['tenantType'] ?? null,
                    'connected_at' => Carbon::now()
                ]
            );
        }
    }
    
    /**
     * Get valid access token (refresh if needed)
     */
    public function getValidToken($userId = null, $forceRefresh = false)
    {
        // First try to get a system-wide token
        $token = XeroToken::whereNull('user_id')
            ->whereNotNull('access_token')
            ->orderBy('connected_at', 'desc')
            ->first();
            
        // If no system token, fall back to user token
        if (!$token && $userId) {
            $token = XeroToken::where('user_id', $userId)
                ->whereNotNull('access_token')
                ->orderBy('connected_at', 'desc')
                ->first();
        }
            
        if (!$token) {
            // No valid token found
            throw new Exception('No valid Xero token found. Please connect to Xero in Admin Integration Settings.');
        }
        
        // Check if token is expired or expires soon (5 minutes buffer)
        if ($token && ($forceRefresh || ($token->expires_at && $token->expires_at->subMinutes(5)->isPast()))) {
            Log::info('Xero token needs refresh', [
                'token_id' => $token->id,
                'expires_at' => $token->expires_at,
                'is_expired' => $token->isExpired(),
                'force_refresh' => $forceRefresh
            ]);
            
            try {
                $this->refreshToken($token);
                $token->refresh();
                
                Log::info('Xero token refreshed successfully', [
                    'token_id' => $token->id,
                    'new_expires_at' => $token->expires_at
                ]);
            } catch (Exception $e) {
                Log::error('Failed to refresh Xero token', [
                    'error' => $e->getMessage(),
                    'token_id' => $token->id,
                    'trace' => $e->getTraceAsString()
                ]);
                throw new Exception('Failed to refresh Xero token. Please reconnect to Xero in Admin Integration Settings.');
            }
        } else {
            Log::info('Xero token is still valid', [
                'token_id' => $token->id,
                'expires_at' => $token->expires_at,
                'is_expired' => $token->isExpired()
            ]);
        }
        
        return $token;
    }
    
    /**
     * Create a new system-wide token using stored credentials
     */
    protected function createSystemToken()
    {
        $clientId = \App\Models\Setting::get('integrations.xero.client_id');
        $clientSecret = \App\Models\Setting::get('integrations.xero.client_secret');
        
        if (!$clientId || !$clientSecret) {
            throw new Exception('Xero credentials not configured in Admin Integration Settings');
        }
        
        // Use authorization code flow instead of client credentials
        // First get the authorization URL
        $authUrl = $this->getAuthorizationUrl();
        
        // Log the attempt to help debugging
        Log::info('Attempting to create system token', [
            'client_id' => $clientId,
            'redirect_uri' => $this->redirectUri,
            'scopes' => $this->scopes
        ]);
        
        // Throw exception to notify admin that manual authorization is needed
        throw new Exception(
            'Manual Xero authorization required. Please go to Admin → Integration Settings → Xero Integration and click "Connect to Xero"'
        );
    }
    
    /**
     * Refresh access token using refresh token
     */
    public function refreshToken($token)
    {
        Log::info('Starting Xero token refresh', [
            'token_id' => $token->id,
            'tenant_id' => $token->tenant_id
        ]);
        
        // Get current credentials from settings
        $clientId = \App\Models\Setting::get('integrations.xero.client_id');
        $clientSecret = \App\Models\Setting::get('integrations.xero.client_secret');
        
        if (!$clientId || !$clientSecret) {
            Log::error('Xero credentials not configured');
            throw new Exception('Xero credentials not configured in Admin Integration Settings');
        }
        
        $response = Http::asForm()->post('https://identity.xero.com/connect/token', [
            'grant_type' => 'refresh_token',
            'client_id' => $clientId,
            'client_secret' => $clientSecret,
            'refresh_token' => $token->refresh_token
        ]);
        
        if (!$response->successful()) {
            Log::error('Xero token refresh failed', [
                'status' => $response->status(),
                'body' => $response->body(),
                'token_id' => $token->id
            ]);
            
            // If refresh fails, try to create a new token
            if ($token->user_id === null) { // Only for system tokens
                throw new Exception('Failed to refresh system token');
            } else {
                throw new Exception('Failed to refresh access token');
            }
        }
        
        $tokenData = $response->json();
        
        $token->update([
            'access_token' => $tokenData['access_token'],
            'refresh_token' => $tokenData['refresh_token'] ?? $token->refresh_token,
            'expires_at' => Carbon::now()->addSeconds($tokenData['expires_in'])
        ]);
    }
    
    /**
     * Get current connection status
     */
    public function getConnectionStatus($userId = null)
    {
        if (!$userId) {
            $userId = auth()->id();
        }
        
        $token = $this->getValidToken($userId);
        
        if (!$token) {
            return [
                'connected' => false,
                'message' => 'Not connected to Xero'
            ];
        }
        
        return [
            'connected' => true,
            'tenant_name' => $token->tenant_name,
            'tenant_id' => $token->tenant_id,
            'connected_at' => $token->connected_at,
            'expires_at' => $token->expires_at
        ];
    }
    
    /**
     * Get tenant ID for API calls
     */
    public function getTenantId($userId = null)
    {
        $token = $this->getValidToken($userId);
        return $token ? $token->tenant_id : null;
    }
    
    /**
     * Disconnect from Xero (revoke tokens)
     */
    public function disconnect($userId = null)
    {
        if (!$userId) {
            $userId = auth()->id();
        }
        
        $tokens = XeroToken::where('user_id', $userId)->get();
        
        foreach ($tokens as $token) {
            // Revoke token at Xero
            try {
                $this->revokeTokenAtXero($token);
            } catch (Exception $e) {
                Log::warning('Failed to revoke Xero token: ' . $e->getMessage());
            }
            
            // Delete from our database
            $token->delete();
        }
    }
    
    /**
     * Revoke all tokens (public method for controller)
     */
    public function revokeToken()
    {
        // Revoke all system tokens
        $tokens = XeroToken::whereNull('user_id')->get();
        
        foreach ($tokens as $token) {
            try {
                $this->revokeTokenAtXero($token);
            } catch (Exception $e) {
                Log::warning('Failed to revoke Xero token: ' . $e->getMessage());
            }
            
            $token->delete();
        }
    }
    
    /**
     * Revoke token at Xero
     */
    protected function revokeTokenAtXero($token)
    {
        $response = Http::asForm()->post('https://identity.xero.com/connect/revocation', [
            'token' => $token->refresh_token,
            'token_type_hint' => 'refresh_token',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret
        ]);
        
        if (!$response->successful()) {
            throw new Exception('Failed to revoke token at Xero');
        }
    }
    
    /**
     * Generate random state for OAuth
     */
    protected function generateState()
    {
        return bin2hex(random_bytes(16));
    }

    /**
     * Create a configured HTTP client for Xero API requests
     * 
     * @param XeroToken|null $token
     * @return \Illuminate\Http\Client\PendingRequest
     */
    public function httpClient($token = null)
    {
        $token ??= $this->getValidToken();
        
        return Http::withHeaders([
            'Authorization'   => 'Bearer ' . $token->access_token,
            'Accept'          => 'application/json',
            'Xero-Tenant-Id'  => $token->tenant_id,
        ])->timeout(config('xero.api.timeout', 30));
    }

    /**
     * Get the base URL for Xero API
     * 
     * @return string
     */
    public function getBaseUrl()
    {
        return config('xero.api.base_url', 'https://api.xero.com/api.xro/2.0');
    }
} 