<?php

namespace App\Services;

use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;

/**
 * Resolves the per-credit-account document-storage backend for a company.
 *
 * A company links to a credit account (cr6f2_ocrcreditaccountmap), which may
 * carry a storage config (cr6f2_storagetype / cr6f2_storageconfig, set in the
 * admin OCR-Credits tab). Two backends:
 *
 *   - SharePoint (resolveSharePointUpload / resolveContextForDocument): the
 *     target folder URL + a Graph token provider that authenticates with the
 *     account's OWN app registration (tenant/client/secret) instead of the
 *     global SYHC token — so each client's documents can land in their own
 *     site. The config mirrors the global SHAREPOINT_* .env block, but
 *     per-account: site_url + document_library + company_root (+ tenant/
 *     client/secret). Upload target is derived PER COMPANY as
 *     {site_url}/{document_library}/{company_root}/{CompanyFolder}/Documents.
 *
 *   - Server / SFTP-FTP (resolveServerUpload): a remote folder path + a
 *     connection config (protocol/host/port/username/decrypted password) for
 *     ServerStorageService. Target derived the same way:
 *     {base_path}/{CompanyFolder}/Documents.
 *
 *   - Google Drive (resolveGoogleDriveUpload): authenticated by a Google
 *     Cloud Service Account (JSON key), no interactive OAuth. Unlike the
 *     other two, Drive addresses folders by opaque id, not path — so the
 *     company/Documents folder chain under the account's root_folder_id is
 *     resolved (created if missing) HERE via GoogleDriveStorageService, and
 *     the caller gets back a ready-to-use folder id.
 *
 * All three return null whenever no usable config of that type exists, so
 * callers fall back to today's behaviour (company invoice folder + global
 * token).
 *
 * Scope note: this currently powers only the primary invoice upload. Downstream
 * reads/moves/OCR still use the global SharePoint token / have no Server or
 * Google Drive support, so a per-account SharePoint target must be reachable
 * by the global app (i.e. a site within SYHC's tenant) to stay end-to-end;
 * Server- and Drive-stored documents can't be OCR'd/previewed yet. True
 * cross-tenant SharePoint and Server/Drive read-back are later phases.
 */
class AccountStorageResolver
{
    public function __construct(
        private DataverseService         $dv,
        private DataverseAuthService     $auth,
        private GoogleDriveStorageService $drive,
    ) {}

    /**
     * @return array{folder_url: string, token_provider: callable}|null
     */
    public function resolveSharePointUpload(?string $companyId): ?array
    {
        if (!$companyId || !Str::isUuid($companyId)) {
            return null;
        }

        try {
            $company = $this->dv->find('cr6f2_companies', $companyId, [
                'select' => ['_cr6f2_ocrcreditaccountmap_value', 'cr6f2_invoicefolder', 'cr6f2_name'],
            ]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read company', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        $creditId = $company['_cr6f2_ocrcreditaccountmap_value'] ?? null;
        if (!$creditId) {
            return null;
        }

        $cfg = $this->storageConfig($creditId, 'sharepoint', $companyId);
        if (!$cfg) {
            return null;
        }

        $tenantId = trim((string) ($cfg['tenant_id'] ?? ''));
        $clientId = trim((string) ($cfg['client_id'] ?? ''));
        $siteUrl  = trim((string) ($cfg['site_url'] ?? ''));
        if ($tenantId === '' || $clientId === '' || $siteUrl === '') {
            return null;
        }

        $secret = $this->decryptSecret($cfg['client_secret'] ?? null, $companyId);
        if ($secret === null) {
            return null;
        }

        $companyFolder = $this->companyFolderName($company);
        if ($companyFolder === '') {
            return null;
        }

        $folderUrl = $this->buildFolderUrl($siteUrl, $cfg, $companyFolder);

        $auth = $this->auth;
        $tokenProvider = fn (bool $force = false): string =>
            $auth->getGraphTokenForCredentials($tenantId, $clientId, $secret, $force);

        return ['folder_url' => $folderUrl, 'token_provider' => $tokenProvider];
    }

    /**
     * Resolve a Server (SFTP/FTP) upload target for a company: the remote
     * folder path plus a connection config for ServerStorageService. Mirrors
     * resolveSharePointUpload — per-company subfolder under the account's
     * base_path, so a multi-company account doesn't dump every company's
     * files into one shared folder. Returns null when the account isn't
     * configured for Server storage (caller falls back to its own default).
     *
     * Scope note: upload-only, same as the SharePoint upload phase — reading
     * these files back (OCR, preview, move/rename/delete) is a follow-up.
     *
     * @return array{folder: string, config: array{protocol: string, host: string, port: ?string, username: ?string, password: string}}|null
     */
    public function resolveServerUpload(?string $companyId): ?array
    {
        if (!$companyId || !Str::isUuid($companyId)) {
            return null;
        }

        try {
            $company = $this->dv->find('cr6f2_companies', $companyId, [
                'select' => ['_cr6f2_ocrcreditaccountmap_value', 'cr6f2_invoicefolder', 'cr6f2_name'],
            ]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read company', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        $creditId = $company['_cr6f2_ocrcreditaccountmap_value'] ?? null;
        if (!$creditId) {
            return null;
        }

        $cfg = $this->storageConfig($creditId, 'server', $companyId);
        if (!$cfg) {
            return null;
        }

        $host = trim((string) ($cfg['host'] ?? ''));
        if ($host === '') {
            return null;
        }

        $password = $this->decryptSecret($cfg['password'] ?? null, $companyId);
        if ($password === null) {
            return null;
        }

        $companyFolder = $this->companyFolderName($company);
        if ($companyFolder === '') {
            return null;
        }

        $basePath = trim((string) ($cfg['base_path'] ?? ''), '/');
        $folder = implode('/', array_filter([$basePath, $companyFolder, 'Documents'], fn ($s) => $s !== ''));

        return [
            'folder' => $folder,
            'direct' => !empty($cfg['direct']),
            'config' => [
                'protocol' => trim((string) ($cfg['protocol'] ?? '')) ?: 'sftp',
                'host'     => $host,
                'port'     => trim((string) ($cfg['port'] ?? '')) ?: null,
                'username' => trim((string) ($cfg['username'] ?? '')) ?: null,
                'password' => $password,
            ],
        ];
    }

    /**
     * Resolve a Google Drive upload target for a company: a resolved parent
     * folder id (root -> company subfolder -> Documents, created if missing)
     * plus the decrypted service-account config for GoogleDriveStorageService.
     *
     * Unlike SharePoint/Server (which build a path string), Drive addresses
     * folders by opaque id — so the company/Documents folder chain is resolved
     * to an id HERE via GoogleDriveStorageService::ensureFolder, not left to
     * the caller. Returns null when the account isn't configured for Google
     * Drive, or the folder chain can't be resolved (e.g. bad credentials, the
     * root folder wasn't actually shared with the service account).
     *
     * Scope note: upload-only, same as SharePoint/Server — reading these files
     * back (OCR, preview, move/rename/delete) is a follow-up.
     *
     * @return array{folder_id: string, config: array{service_account_json: string}}|null
     */
    public function resolveGoogleDriveUpload(?string $companyId): ?array
    {
        if (!$companyId || !Str::isUuid($companyId)) {
            return null;
        }

        try {
            $company = $this->dv->find('cr6f2_companies', $companyId, [
                'select' => ['_cr6f2_ocrcreditaccountmap_value', 'cr6f2_invoicefolder', 'cr6f2_name'],
            ]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read company', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        $creditId = $company['_cr6f2_ocrcreditaccountmap_value'] ?? null;
        if (!$creditId) {
            return null;
        }

        $cfg = $this->storageConfig($creditId, 'googledrive', $companyId);
        if (!$cfg) {
            return null;
        }

        $rootFolderId = trim((string) ($cfg['root_folder_id'] ?? ''));
        if ($rootFolderId === '') {
            return null;
        }

        $json = $this->decryptSecret($cfg['service_account_json'] ?? null, $companyId);
        if ($json === null) {
            return null;
        }

        $companyFolder = $this->companyFolderName($company);
        if ($companyFolder === '') {
            return null;
        }

        $config = ['service_account_json' => $json];

        try {
            $companyFolderId   = $this->drive->ensureFolder($config, $rootFolderId, $companyFolder);
            $documentsFolderId = $this->drive->ensureFolder($config, $companyFolderId, 'Documents');
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: Google Drive folder resolution failed', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        return ['folder_id' => $documentsFolderId, 'direct' => !empty($cfg['direct']), 'config' => $config];
    }

    /**
     * Direct (zero-copy) upload target: the client backend to write the PRIMARY
     * copy into (its Documents folder becomes cr6f2_documentlink), for an account
     * that opted OUT of keeping a working copy in SYHC SharePoint. Null when the
     * account is not in direct mode — the caller then uses the normal SharePoint +
     * mirror path. Only server / Google Drive can be direct (a per-account
     * SharePoint client already stores natively in their own tenant).
     *
     * @return array{backend:string,config:array,folder?:string,folder_id?:string}|null
     */
    public function resolveDirectUpload(?string $companyId): ?array
    {
        if (!$companyId || !Str::isUuid($companyId)) {
            return null;
        }
        if (($s = $this->resolveServerUpload($companyId)) && !empty($s['direct'])) {
            return ['backend' => 'server', 'config' => $s['config'], 'folder' => $s['folder']];
        }
        if (($d = $this->resolveGoogleDriveUpload($companyId)) && !empty($d['direct'])) {
            return ['backend' => 'googledrive', 'config' => $d['config'], 'folder_id' => $d['folder_id']];
        }
        return null;
    }

    /**
     * Client backend connection for MIRRORING a document into a company's own
     * storage from the Python OCR service (split children, SplitOriginal, rotate).
     * server → protocol/host/port/username/password + the company's Documents
     * folder; googledrive → a short-lived access token + the Documents folder id.
     * Null when the company uses SYHC SharePoint (nothing to mirror).
     *
     * @return array{backend:string}|null
     */
    public function resolveClientMirrorContext(?string $companyId): ?array
    {
        if (!$companyId || !Str::isUuid($companyId)) {
            return null;
        }
        if ($s = $this->resolveServerUpload($companyId)) {
            $c = $s['config'];
            return [
                'backend'  => 'server',
                'protocol' => $c['protocol'] ?? 'sftp',
                'host'     => $c['host'] ?? '',
                'port'     => $c['port'] ?? null,
                'username' => $c['username'] ?? null,
                'password' => $c['password'] ?? '',
                'folder'   => $s['folder'],
                // Zero-copy account: the client backend is the PRIMARY store, not a
                // mirror. The Python OCR service writes split children there as the
                // only copy (documentlink = client link) instead of SharePoint.
                'direct'   => !empty($s['direct']),
            ];
        }
        if ($d = $this->resolveGoogleDriveUpload($companyId)) {
            try {
                $token = $this->drive->mintAccessToken($d['config']);
            } catch (Throwable $e) {
                Log::warning('AccountStorageResolver: Drive mirror token mint failed', ['company_id' => $companyId, 'error' => $e->getMessage()]);
                return null;
            }
            return ['backend' => 'googledrive', 'access_token' => $token, 'folder_id' => $d['folder_id'], 'direct' => !empty($d['direct'])];
        }
        return null;
    }

    /** resolveClientMirrorContext for a document (looks up its company). */
    public function resolveClientMirrorContextForDocument(string $documentId): ?array
    {
        if (!Str::isUuid($documentId)) {
            return null;
        }
        try {
            $doc = $this->dv->find('cr6f2_documentses', $documentId, ['select' => ['_cr6f2_companyid_value']]);
        } catch (Throwable $e) {
            return null;
        }
        return $this->resolveClientMirrorContext($doc['_cr6f2_companyid_value'] ?? null);
    }

    /**
     * Storage context for a document — a ready-to-use credential/token for
     * whichever backend the document's credit account uses. Used by the
     * internal endpoint the Python OCR pipeline calls to read a document back:
     * PHP keeps every secret + APP_KEY; the caller only ever receives a
     * short-lived token or a plain server credential, never the raw secret.
     * Returns null when the account has no storage config (caller uses its
     * own global default: SYHC's SharePoint tenant + root site).
     *
     * @return array{backend:string}|null  Shape depends on 'backend':
     *   sharepoint  → {backend, access_token, host, site_url, document_library, company_root}
     *   server      → {backend, protocol, host, port, username, password}
     *   googledrive → {backend, access_token}
     */
    public function resolveContextForDocument(string $documentId): ?array
    {
        if (!Str::isUuid($documentId)) {
            return null;
        }
        try {
            $doc = $this->dv->find('cr6f2_documentses', $documentId, ['select' => ['cr6f2_documentlink', '_cr6f2_companyid_value']]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read document', ['document_id' => $documentId, 'error' => $e->getMessage()]);
            return null;
        }
        // Dispatch on where the file ACTUALLY lives (the link scheme), not the
        // account's configured storage_type: a client-storage account's OCR working
        // copy sits in SYHC SharePoint (https), so it must be read with SYHC's own
        // creds, not the account's server/Drive credential.
        $scheme = strtolower((string) (parse_url((string) ($doc['cr6f2_documentlink'] ?? ''), PHP_URL_SCHEME) ?: ''));
        return $this->resolveTokenContext($doc['_cr6f2_companyid_value'] ?? null, $scheme);
    }

    /**
     * Storage context for a company (see resolveContextForDocument). Reads the
     * credit account's storage_type once, then dispatches to the matching
     * per-backend token builder.
     *
     * @return array{backend:string}|null
     */
    public function resolveTokenContext(?string $companyId, ?string $linkScheme = null): ?array
    {
        if (!$companyId || !Str::isUuid($companyId)) {
            return null;
        }
        try {
            $company = $this->dv->find('cr6f2_companies', $companyId, ['select' => ['_cr6f2_ocrcreditaccountmap_value']]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read company', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }
        $creditId = $company['_cr6f2_ocrcreditaccountmap_value'] ?? null;
        if (!$creditId) {
            return null;
        }

        try {
            $credit = $this->dv->find('crf61_cr6f2_ocrcreditses', $creditId, ['select' => ['cr6f2_storagetype', 'cr6f2_storageconfig']]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read storage config', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }
        $type = $credit['cr6f2_storagetype'] ?? null;
        $cfg  = json_decode($credit['cr6f2_storageconfig'] ?? '', true);
        if (!is_array($cfg)) {
            return null;
        }

        // When the document's link scheme is known, dispatch on WHERE THE FILE IS
        // (the scheme) rather than the account's configured storage_type. For a
        // client-storage account whose OCR working copy sits in SYHC SharePoint
        // (https), this picks the SharePoint builder — which, handed the server/
        // Drive config, finds no SharePoint fields and returns null, i.e. the global
        // SYHC creds (correct: the working copy is in our own tenant). A degraded doc
        // still on the client backend (sftp/ftp/googledrive link) resolves the
        // matching client credential.
        $target = match ($linkScheme) {
            'sftp', 'ftp'   => 'server',
            'googledrive'   => 'googledrive',
            'http', 'https' => 'sharepoint',
            default         => $type,     // no scheme hint → account's configured type
        };

        return match ($target) {
            'sharepoint'  => $this->sharePointTokenContext($cfg, $companyId),
            'server'      => $this->serverTokenContext($cfg, $companyId),
            'googledrive' => $this->googleDriveTokenContext($cfg, $companyId),
            default       => null,
        };
    }

    private function sharePointTokenContext(array $cfg, string $companyId): ?array
    {
        $tenantId = trim((string) ($cfg['tenant_id'] ?? ''));
        $clientId = trim((string) ($cfg['client_id'] ?? ''));
        $siteUrl  = trim((string) ($cfg['site_url'] ?? ''));
        if ($tenantId === '' || $clientId === '' || $siteUrl === '') {
            return null;
        }
        $secret = $this->decryptSecret($cfg['client_secret'] ?? null, $companyId);
        if ($secret === null) {
            return null;
        }

        try {
            $token = $this->auth->getGraphTokenForCredentials($tenantId, $clientId, $secret);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: SharePoint token mint failed', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        return [
            'backend'          => 'sharepoint',
            'access_token'     => $token,
            'host'             => parse_url($siteUrl, PHP_URL_HOST) ?: '',
            'site_url'         => $siteUrl,
            'document_library' => trim((string) ($cfg['document_library'] ?? '')) ?: 'Shared Documents',
            'company_root'     => trim((string) ($cfg['company_root'] ?? '')),
        ];
    }

    private function serverTokenContext(array $cfg, string $companyId): ?array
    {
        $host = trim((string) ($cfg['host'] ?? ''));
        if ($host === '') {
            return null;
        }
        $password = $this->decryptSecret($cfg['password'] ?? null, $companyId);
        if ($password === null) {
            return null;
        }

        return [
            'backend'  => 'server',
            'protocol' => trim((string) ($cfg['protocol'] ?? '')) ?: 'sftp',
            'host'     => $host,
            'port'     => trim((string) ($cfg['port'] ?? '')) ?: null,
            'username' => trim((string) ($cfg['username'] ?? '')) ?: null,
            'password' => $password,
        ];
    }

    private function googleDriveTokenContext(array $cfg, string $companyId): ?array
    {
        $json = $this->decryptSecret($cfg['service_account_json'] ?? null, $companyId);
        if ($json === null) {
            return null;
        }

        try {
            $token = $this->drive->mintAccessToken(['service_account_json' => $json]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: Google Drive token mint failed', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        return ['backend' => 'googledrive', 'access_token' => $token];
    }

    /**
     * Decoded storage config for a credit account, or null if the account
     * isn't configured for the given backend type ('sharepoint' | 'server').
     *
     * @return array<string,mixed>|null
     */
    private function storageConfig(string $creditId, string $type, string $companyId): ?array
    {
        try {
            $credit = $this->dv->find('crf61_cr6f2_ocrcreditses', $creditId, ['select' => ['cr6f2_storagetype', 'cr6f2_storageconfig']]);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: failed to read storage config', ['company_id' => $companyId, 'error' => $e->getMessage()]);
            return null;
        }

        if (($credit['cr6f2_storagetype'] ?? null) !== $type) {
            return null;
        }

        $cfg = json_decode($credit['cr6f2_storageconfig'] ?? '', true);
        return is_array($cfg) ? $cfg : null;
    }

    /**
     * The company's own folder name — the last segment of its stored invoice
     * folder (already the canonical, dot-stripped name), falling back to the
     * company name. Each company gets this subfolder under the account root.
     */
    private function companyFolderName(array $company): string
    {
        $invoiceFolder = trim((string) ($company['cr6f2_invoicefolder'] ?? ''));
        if ($invoiceFolder !== '') {
            $segments = explode('/', rtrim($invoiceFolder, '/'));
            $last = end($segments);
            if (is_string($last) && $last !== '') {
                return $last;
            }
        }
        return trim(rtrim((string) ($company['cr6f2_name'] ?? ''), '. '));
    }

    /**
     * Build {site_url}/{library}/{company_root}/{CompanyFolder}/Documents.
     * Accepts a company_root that already includes the library prefix (verbatim
     * .env style) OR one relative to the library — both yield the same path.
     */
    private function buildFolderUrl(string $siteUrl, array $cfg, string $companyFolder): string
    {
        $library = trim((string) ($cfg['document_library'] ?? '')) ?: 'Shared Documents';
        $root    = trim((string) ($cfg['company_root'] ?? ''), '/');

        if ($root !== '' && stripos($root, $library . '/') === 0) {
            $root = substr($root, strlen($library) + 1);
        }

        $segments = array_values(array_filter(
            [$library, $root, $companyFolder, 'Documents'],
            fn ($s) => $s !== '' && $s !== null
        ));

        return rtrim($siteUrl, '/') . '/' . implode('/', $segments);
    }

    private function decryptSecret(?string $ciphertext, string $companyId): ?string
    {
        if (!$ciphertext) {
            return null;
        }
        try {
            return Crypt::decryptString($ciphertext);
        } catch (Throwable $e) {
            Log::warning('AccountStorageResolver: secret decrypt failed', [
                'company_id' => $companyId,
                'error'      => $e->getMessage(),
            ]);
            return null;
        }
    }
}
