// *****************************************************************************
// Copyright (C) 2021 EclipseSource and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { ConfirmDialog, Dialog, StorageService } from '@theia/core/lib/browser';
import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string';
import { StatusBar, StatusBarAlignment } from '@theia/core/lib/browser/status-bar/status-bar';
import { OS, ContributionProvider, DisposableCollection } from '@theia/core';
import { Emitter, Event } from '@theia/core/lib/common';
import URI from '@theia/core/lib/common/uri';
import { PreferenceChange, PreferenceSchemaService, PreferenceScope, PreferenceService } from '@theia/core/lib/common/preferences';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { inject, injectable, named, postConstruct, preDestroy } from '@theia/core/shared/inversify';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import {
    WorkspaceTrustPreferences, WORKSPACE_TRUST_EMPTY_WINDOW, WORKSPACE_TRUST_ENABLED, WORKSPACE_TRUST_STARTUP_PROMPT, WORKSPACE_TRUST_TRUSTED_FOLDERS, WorkspaceTrustPrompt
} from '../common/workspace-trust-preferences';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { WorkspaceService } from './workspace-service';
import { WorkspaceCommands } from './workspace-commands';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { WorkspaceTrustDialog } from './workspace-trust-dialog';
import { UntitledWorkspaceService } from '../common/untitled-workspace-service';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';

const STORAGE_TRUSTED = 'trusted';
export const WORKSPACE_TRUST_STATUS_BAR_ID = 'workspace-trust-status';

/**
 * Contribution interface for features that are restricted in untrusted workspaces.
 * Implementations can provide information about what is being restricted.
 */
export const WorkspaceRestrictionContribution = Symbol('WorkspaceRestrictionContribution');
export interface WorkspaceRestrictionContribution {
    /**
     * Returns the restrictions currently active due to workspace trust.
     * Called when building the restricted mode status bar tooltip.
     */
    getRestrictions(): WorkspaceRestriction[];

    /**
     * Returns whether a window reload is required when workspace trust changes to `newTrust`.
     * Called before reloading on trust change to avoid unnecessary reloads when no
     * trust-restricted items are actually affected.
     * If not implemented, the contribution is assumed not to require a reload.
     */
    requiresReloadOnTrustChange?(newTrust: boolean): boolean;
}

export interface WorkspaceRestriction {
    /** Display name of the feature being restricted */
    label: string;
    /** Optional details (e.g., list of blocked items) */
    details?: string[];
}

@injectable()
export class WorkspaceTrustService {
    @inject(WorkspaceService)
    protected readonly workspaceService: WorkspaceService;

    @inject(PreferenceService)
    protected readonly preferences: PreferenceService;

    @inject(StorageService)
    protected readonly storage: StorageService;

    @inject(MessageService)
    protected readonly messageService: MessageService;

    @inject(WorkspaceTrustPreferences)
    protected readonly workspaceTrustPref: WorkspaceTrustPreferences;

    @inject(PreferenceSchemaService)
    protected readonly preferenceSchemaService: PreferenceSchemaService;

    @inject(WindowService)
    protected readonly windowService: WindowService;

    @inject(ContextKeyService)
    protected readonly contextKeyService: ContextKeyService;

    @inject(StatusBar)
    protected readonly statusBar: StatusBar;

    @inject(ContributionProvider) @named(WorkspaceRestrictionContribution)
    protected readonly restrictionContributions: ContributionProvider<WorkspaceRestrictionContribution>;

    @inject(UntitledWorkspaceService)
    protected readonly untitledWorkspaceService: UntitledWorkspaceService;

    @inject(EnvVariablesServer)
    protected readonly envVariablesServer: EnvVariablesServer;

    protected workspaceTrust = new Deferred<boolean>();
    protected currentTrust: boolean | undefined;
    protected pendingTrustDialog: Deferred<boolean> | undefined;
    protected pendingTrustRequest: Deferred<boolean | undefined> | undefined;
    /**
     * Effective value of `security.workspace.trust.enabled` at the time trust was last resolved.
     * Used to distinguish a real user change from the initial-load preference change event
     * that the User provider emits asynchronously after start-up.
     */
    protected lastKnownTrustEnabled: boolean | undefined;

    protected readonly onDidChangeWorkspaceTrustEmitter = new Emitter<boolean>();
    readonly onDidChangeWorkspaceTrust: Event<boolean> = this.onDidChangeWorkspaceTrustEmitter.event;

    protected readonly toDispose = new DisposableCollection(this.onDidChangeWorkspaceTrustEmitter);

    @postConstruct()
    protected init(): void {
        this.doInit();
    }

    protected async doInit(): Promise<void> {
        await this.workspaceService.ready;
        await this.workspaceTrustPref.ready;
        await this.preferenceSchemaService.ready;
        await this.resolveWorkspaceTrust();
        this.toDispose.push(
            this.preferences.onPreferenceChanged(change => this.handlePreferenceChange(change))
        );
        this.toDispose.push(
            this.workspaceService.onWorkspaceChanged(() => this.handleWorkspaceChanged())
        );

        // Show status bar item if starting in restricted mode
        const initialTrust = await this.getWorkspaceTrust();
        this.updateRestrictedModeIndicator(initialTrust);

        // React to trust changes
        this.toDispose.push(
            this.onDidChangeWorkspaceTrust(trust => {
                this.updateRestrictedModeIndicator(trust);
            })
        );
    }

    @preDestroy()
    protected onStop(): void {
        this.toDispose.dispose();
    }

    getWorkspaceTrust(): Promise<boolean> {
        // Return current trust if already resolved, otherwise wait for initial resolution
        if (this.currentTrust !== undefined) {
            return Promise.resolve(this.currentTrust);
        }
        return this.workspaceTrust.promise;
    }

    protected async resolveWorkspaceTrust(givenTrust?: boolean): Promise<void> {
        if (!this.isWorkspaceTrustResolved()) {
            this.lastKnownTrustEnabled = !!this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
            const trust = givenTrust ?? await this.calculateWorkspaceTrust();
            if (trust !== undefined) {
                await this.storeWorkspaceTrust(trust);
                this.contextKeyService.setContext('isWorkspaceTrusted', trust);
                this.currentTrust = trust;
                this.workspaceTrust.resolve(trust);
                this.onDidChangeWorkspaceTrustEmitter.fire(trust);
                if (trust && this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
                    await this.addToTrustedFolders();
                }
            }
        }
    }

    async setWorkspaceTrust(trusted: boolean, reload = true): Promise<void> {
        if (this.currentTrust === trusted) {
            return;
        }
        const needsReload = reload && this.shouldReloadForTrustChange(trusted);
        if (needsReload && !await this.confirmRestart()) {
            return;
        }
        // Must be set before add/removeFromTrustedFolders to prevent handlePreferenceChange from recursing.
        this.currentTrust = trusted;
        this.contextKeyService.setContext('isWorkspaceTrusted', trusted);
        this.onDidChangeWorkspaceTrustEmitter.fire(trusted);
        await this.storeWorkspaceTrust(trusted);
        await (trusted ? this.addToTrustedFolders() : this.removeFromTrustedFolders());
        if (needsReload) {
            this.windowService.reload();
        }
    }

    protected shouldReloadForTrustChange(newTrust: boolean): boolean {
        return this.restrictionContributions.getContributions()
            .some(c => c.requiresReloadOnTrustChange?.(newTrust) ?? false);
    }

    protected isWorkspaceTrustResolved(): boolean {
        return this.workspaceTrust.state !== 'unresolved';
    }

    protected async calculateWorkspaceTrust(): Promise<boolean | undefined> {
        const trustEnabled = this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
        if (!trustEnabled) {
            return true;
        }

        // Empty workspace - no folders open
        if (await this.isEmptyWorkspace()) {
            return !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
        }

        if (await this.areAllWorkspaceUrisTrusted()) {
            return true;
        }

        if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.NEVER) {
            return false;
        }

        // For ONCE mode, check stored trust first
        if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
            const storedTrust = await this.loadWorkspaceTrust();
            if (storedTrust !== undefined) {
                return storedTrust;
            }
        }

        // For ALWAYS mode or ONCE mode with no stored decision, show dialog
        return this.showTrustPromptDialog();
    }

    /**
     * Check if the workspace is empty (no workspace or folder opened, or
     * an untitled workspace with no folders).
     * A saved workspace file with 0 folders is NOT empty - it still needs trust
     * evaluation because it could have tasks defined.
     */
    protected async isEmptyWorkspace(): Promise<boolean> {
        const workspace = this.workspaceService.workspace;
        if (!workspace) {
            return true;
        }
        const roots = this.workspaceService.tryGetRoots();
        // Only consider it empty if it's an untitled workspace with no folders
        // Use secure check with configDirUri for trust-related decisions
        if (roots.length === 0) {
            const configDirUri = new URI(await this.envVariablesServer.getConfigDirUri());
            if (this.untitledWorkspaceService.isUntitledWorkspace(workspace.resource, configDirUri)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the URIs that need to be trusted for the current workspace.
     * This includes all workspace folder URIs, plus the workspace file URI
     * for saved workspaces (since workspace files can contain tasks/settings).
     */
    protected getWorkspaceUris(): URI[] {
        const uris = this.workspaceService.tryGetRoots().map(root => root.resource);
        const workspace = this.workspaceService.workspace;
        // For saved workspaces, include the workspace file itself
        if (workspace && this.workspaceService.saved) {
            uris.push(workspace.resource);
        }
        return uris;
    }

    /**
     * Check if all workspace URIs are trusted.
     * A workspace is trusted only if ALL of its folders (and the workspace
     * file for saved workspaces) are trusted.
     */
    protected async areAllWorkspaceUrisTrusted(): Promise<boolean> {
        const uris = this.getWorkspaceUris();
        if (uris.length === 0) {
            return false;
        }
        return uris.every(uri => this.isUriTrusted(uri));
    }

    /**
     * Check if a URI is trusted. A URI is trusted if it or any of its
     * parent folders is in the trusted folders list.
     */
    protected isUriTrusted(uri: URI): boolean {
        const trustedFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
        const caseSensitive = !OS.backend.isWindows;
        const normalizedUri = uri.normalizePath();

        return trustedFolders.some(folder => {
            try {
                const folderUri = new URI(folder).normalizePath();
                // Check if the trusted folder is equal to or a parent of the URI
                return folderUri.isEqualOrParent(normalizedUri, caseSensitive);
            } catch {
                return false; // Invalid URI in preferences
            }
        });
    }

    protected async showTrustPromptDialog(): Promise<boolean> {
        // If dialog is already open, wait for its result
        if (this.pendingTrustDialog) {
            return this.pendingTrustDialog.promise;
        }

        this.pendingTrustDialog = new Deferred<boolean>();
        try {
            // Show the workspace folders in the dialog
            const folderUris = this.workspaceService.tryGetRoots().map(root => root.resource);

            const dialog = new WorkspaceTrustDialog(folderUris);

            const result = await dialog.open();
            const trusted = result === true;
            this.pendingTrustDialog.resolve(trusted);
            return trusted;
        } catch (e) {
            this.pendingTrustDialog.resolve(false);
            throw e;
        } finally {
            this.pendingTrustDialog = undefined;
        }
    }

    async addToTrustedFolders(): Promise<void> {
        const uris = this.getWorkspaceUris();
        if (uris.length === 0) {
            return;
        }

        const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
        const newFolders = [...currentFolders];
        let changed = false;

        for (const uri of uris) {
            if (!this.isUriTrusted(uri)) {
                newFolders.push(uri.toString());
                changed = true;
            }
        }

        if (changed) {
            await this.preferences.set(
                WORKSPACE_TRUST_TRUSTED_FOLDERS,
                newFolders,
                PreferenceScope.User
            );
        }
    }

    async removeFromTrustedFolders(): Promise<void> {
        const uris = this.getWorkspaceUris();
        if (uris.length === 0) {
            return;
        }

        const currentFolders = this.workspaceTrustPref[WORKSPACE_TRUST_TRUSTED_FOLDERS] || [];
        const caseSensitive = !OS.backend.isWindows;
        const normalizedUris = uris.map(uri => uri.normalizePath());

        const updatedFolders = currentFolders.filter(folder => {
            try {
                const folderUri = new URI(folder).normalizePath();
                // Remove folder if it exactly matches any workspace URI
                return !normalizedUris.some(wsUri => wsUri.isEqual(folderUri, caseSensitive));
            } catch {
                return true; // Keep invalid URIs
            }
        });

        if (updatedFolders.length !== currentFolders.length) {
            await this.preferences.set(
                WORKSPACE_TRUST_TRUSTED_FOLDERS,
                updatedFolders,
                PreferenceScope.User
            );
        }
    }

    protected async loadWorkspaceTrust(): Promise<boolean | undefined> {
        if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
            return this.storage.getData<boolean>(STORAGE_TRUSTED);
        }
    }

    protected async storeWorkspaceTrust(trust: boolean): Promise<void> {
        if (this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] === WorkspaceTrustPrompt.ONCE) {
            return this.storage.setData(STORAGE_TRUSTED, trust);
        }
    }

    protected async handlePreferenceChange(change: PreferenceChange): Promise<void> {
        // Handle trustedFolders changes regardless of scope
        if (change.preferenceName === WORKSPACE_TRUST_TRUSTED_FOLDERS) {
            // When trust is disabled, trust is unconditionally granted: trustedFolders
            // changes must not flip `currentTrust` (which would trigger a restart dialog
            // via `setWorkspaceTrust` → `shouldReloadForTrustChange`).
            if (!this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED]) {
                return;
            }
            // For empty windows with emptyWindow setting enabled, trust should remain true
            if (await this.isEmptyWorkspace() && this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW]) {
                return;
            }
            const areAllUrisTrusted = await this.areAllWorkspaceUrisTrusted();
            if (areAllUrisTrusted !== this.currentTrust) {
                await this.setWorkspaceTrust(areAllUrisTrusted);
            }
            return;
        }

        if (change.scope === PreferenceScope.User) {
            if (change.preferenceName === WORKSPACE_TRUST_STARTUP_PROMPT && this.workspaceTrustPref[WORKSPACE_TRUST_STARTUP_PROMPT] !== WorkspaceTrustPrompt.ONCE) {
                this.storage.setData(STORAGE_TRUSTED, undefined);
            }

            if (change.preferenceName === WORKSPACE_TRUST_ENABLED) {
                const currentEnabled = !!this.workspaceTrustPref[WORKSPACE_TRUST_ENABLED];
                // The preference framework emits a User-scope change event when the user
                // settings file is first loaded (oldValue undefined → current value). Only
                // prompt for restart when the effective value has actually changed since
                // trust was last resolved.
                if (currentEnabled !== this.lastKnownTrustEnabled) {
                    this.lastKnownTrustEnabled = currentEnabled;
                    if (!await this.isEmptyWorkspace() && this.isWorkspaceTrustResolved() && await this.confirmRestart()) {
                        this.windowService.reload();
                    }
                }
                this.resolveWorkspaceTrust();
            }

            // Handle emptyWindow setting change for empty windows
            if (change.preferenceName === WORKSPACE_TRUST_EMPTY_WINDOW && await this.isEmptyWorkspace()) {
                // For empty windows, directly update trust based on the new setting value
                const shouldTrust = !!this.workspaceTrustPref[WORKSPACE_TRUST_EMPTY_WINDOW];
                if (this.currentTrust !== shouldTrust) {
                    // No reload needed: in an empty window there are no workspace folders,
                    // so no extensions are blocked by trust and no extension host restart is required.
                    await this.setWorkspaceTrust(shouldTrust, false);
                }
            }
        }
    }

    protected async handleWorkspaceChanged(): Promise<void> {
        // Reset trust state for the new workspace
        this.workspaceTrust = new Deferred<boolean>();
        this.currentTrust = undefined;

        // Re-evaluate trust for the new workspace
        await this.resolveWorkspaceTrust();

        // Update status bar indicator
        const trust = await this.getWorkspaceTrust();
        this.updateRestrictedModeIndicator(trust);
    }

    protected async confirmRestart(): Promise<boolean> {
        const shouldRestart = await new ConfirmDialog({
            title: nls.localizeByDefault('A setting has changed that requires a restart to take effect.'),
            msg: nls.localizeByDefault('Press the restart button to restart {0} and enable the setting.', FrontendApplicationConfigProvider.get().applicationName),
            ok: nls.localizeByDefault('Restart'),
            cancel: Dialog.CANCEL,
        }).open();
        return shouldRestart === true;
    }

    protected updateRestrictedModeIndicator(trusted: boolean): void {
        if (trusted) {
            this.hideRestrictedModeStatusBarItem();
        } else {
            this.showRestrictedModeStatusBarItem();
        }
    }

    protected showRestrictedModeStatusBarItem(): void {
        this.statusBar.setElement(WORKSPACE_TRUST_STATUS_BAR_ID, {
            text: '$(shield) ' + nls.localizeByDefault('Restricted Mode'),
            alignment: StatusBarAlignment.LEFT,
            backgroundColor: 'var(--theia-statusBarItem-prominentBackground)',
            color: 'var(--theia-statusBarItem-prominentForeground)',
            priority: 5000,
            tooltip: this.createRestrictedModeTooltip(),
            command: WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id
        });
    }

    protected createRestrictedModeTooltip(): MarkdownString {
        const manageTrustLink = `[${WorkspaceCommands.MANAGE_WORKSPACE_TRUST.label}](command:${WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id})`;
        const docsLink = `[${nls.localize('theia/workspace/trustLearnMore', "Learn more about Theia's Workspace Trust")}](https://theia-ide.org/docs/workspace_trust/)`;

        const md = new MarkdownStringImpl('', {
            supportThemeIcons: true,
            isTrusted: { enabledCommands: [WorkspaceCommands.MANAGE_WORKSPACE_TRUST.id] }
        });

        md.appendMarkdown(`**${nls.localizeByDefault('Restricted Mode')}**\n\n`);

        md.appendMarkdown(nls.localize('theia/workspace/restrictedModeDescription',
            'Some features are disabled because this workspace is not trusted.'));
        md.appendMarkdown('\n\n');
        md.appendMarkdown(nls.localize('theia/workspace/restrictedModeFeatures',
            'Features like task execution, debugging, extensions, and AI are disabled to protect against potentially harmful code.'));
        md.appendMarkdown('\n\n');
        md.appendMarkdown(manageTrustLink);

        const restrictions = this.collectRestrictions();
        if (restrictions.length > 0) {
            md.appendMarkdown('\n\n---\n\n');
            for (const restriction of restrictions) {
                md.appendMarkdown(`**${restriction.label}**\n\n`);
                if (restriction.details && restriction.details.length > 0) {
                    for (const detail of restriction.details) {
                        md.appendMarkdown(`- ${detail}\n`);
                    }
                    md.appendMarkdown('\n');
                }
            }
        }

        md.appendMarkdown('\n\n---\n\n');
        md.appendMarkdown(docsLink);

        return md;
    }

    protected collectRestrictions(): WorkspaceRestriction[] {
        const restrictions: WorkspaceRestriction[] = [];
        for (const contribution of this.restrictionContributions.getContributions()) {
            restrictions.push(...contribution.getRestrictions());
        }
        return restrictions;
    }

    protected hideRestrictedModeStatusBarItem(): void {
        this.statusBar.removeElement(WORKSPACE_TRUST_STATUS_BAR_ID);
    }

    /**
     * Refreshes the restricted mode status bar item.
     * Call this when restriction contributions change.
     */
    refreshRestrictedModeIndicator(): void {
        if (this.currentTrust === false) {
            this.showRestrictedModeStatusBarItem();
        }
    }

    /**
     * Request workspace trust from the user. This method follows VS Code's pattern:
     * - If already trusted, returns true immediately
     * - If there's already a pending trust request, returns the same promise (avoiding duplicate dialogs)
     * - Otherwise, shows a dialog and waits for the user's response
     *
     * Unlike the initial trust resolution, this can be called multiple times and will
     * prompt the user each time (unless a dialog is already open).
     */
    async requestWorkspaceTrust(): Promise<boolean | undefined> {
        // If already trusted, return true immediately
        if (this.currentTrust === true) {
            return true;
        }

        // If there's already a pending request, return the same promise to avoid duplicate dialogs
        if (this.pendingTrustRequest) {
            return this.pendingTrustRequest.promise;
        }

        // Create a new pending request
        this.pendingTrustRequest = new Deferred<boolean | undefined>();

        try {
            const grantedTrust = await this.showTrustPromptDialog();
            if (grantedTrust) {
                await this.setWorkspaceTrust(true);
            }
            this.pendingTrustRequest.resolve(grantedTrust);
            return grantedTrust;
        } catch (e) {
            this.pendingTrustRequest.resolve(undefined);
            throw e;
        } finally {
            this.pendingTrustRequest = undefined;
        }
    }
}
