import {AppContext, AppContextReact, DataLoader, NavigationManager, Notifications, NotificationsManager, PageContext, Popup, PopupManager, PopupProps} from 'argo-ui';
import {createBrowserHistory} from 'history';
import * as React from 'react';
import {Helmet} from 'react-helmet';
import {Redirect, Route, RouteComponentProps, Router, Switch} from 'react-router';
import {Subscription} from 'rxjs';
import applications from './applications';
import help from './help';
import login from './login';
import settings from './settings';
import {Layout, ThemeWrapper} from './shared/components/layout/layout';
import {Page} from './shared/components';
import {Spinner} from './shared/components';
import {VersionPanel} from './shared/components/version-info/version-info-panel';
import {AuthSettingsCtx, Provider} from './shared/context';
import {services} from './shared/services';
import requests from './shared/services/requests';
import {hashCode, isSSOConfigured} from './shared/utils';
import {Banner} from './ui-banner/ui-banner';
import userInfo from './user-info';
import {AuthSettings, UserInfo} from './shared/models';
import {SystemLevelExtension} from './shared/services/extensions-service';

services.viewPreferences.init();
const bases = document.getElementsByTagName('base');
const base = bases.length > 0 ? bases[0].getAttribute('href') || '/' : '/';
export const history = createBrowserHistory({basename: base});
requests.setBaseHRef(base);

type Routes = {[path: string]: {component: React.ComponentType<RouteComponentProps<any>>; noLayout?: boolean}};

const routes: Routes = {
    '/login': {component: login.component as any, noLayout: true},
    '/applications': {component: applications.component},
    // TODO: Uncomment when ApplicationSet details page is fully implemented
    '/applicationsets': {component: applications.component},
    '/settings': {component: settings.component},
    '/user-info': {component: userInfo.component},
    '/help': {component: help.component}
};

interface NavItem {
    title: string;
    tooltip?: string;
    path: string;
    iconClassName: string;
}

const navItems: NavItem[] = [
    {
        title: 'Applications',
        tooltip: 'Manage your applications, and diagnose health problems.',
        path: '/applications',
        iconClassName: 'argo-icon argo-icon-application'
    },
    {
        title: 'ApplicationSets',
        tooltip: 'Manage your ApplicationSets, and diagnose health problems.',
        path: '/applicationsets',
        iconClassName: 'argo-icon argo-icon-applicationset'
    },
    {
        title: 'Settings',
        tooltip: 'Manage your repositories, projects, settings',
        path: '/settings',
        iconClassName: 'argo-icon argo-icon-settings'
    },
    {
        title: 'User Info',
        path: '/user-info',
        iconClassName: 'fa fa-user-circle'
    },
    {
        title: 'Documentation',
        tooltip: 'Read the documentation, and get help and assistance.',
        path: '/help',
        iconClassName: 'argo-icon argo-icon-docs'
    }
];

const versionLoader = services.version.version();

async function fetchUserInfoSafe(): Promise<UserInfo> {
    try {
        return await services.users.get();
    } catch (err: any) {
        if (err?.response?.status === 401 || err?.status === 401) {
            return {loggedIn: false, username: '', iss: 'argocd', groups: []};
        }
        throw err;
    }
}

function sessionFromBootstrap(userInfo: UserInfo, authSettings: AuthSettings, versionInfo: any): {loggedIn: boolean; isSSO: boolean} {
    const loggedIn = userInfo?.loggedIn ?? false;
    if (loggedIn) {
        return {loggedIn: true, isSSO: false};
    }
    const serverAuthDisabled = !loggedIn && !!(versionInfo?.KustomizeVersion?.length || versionInfo?.HelmVersion?.length);
    if (serverAuthDisabled) {
        return {loggedIn: true, isSSO: false};
    }
    const hasSSO = isSSOConfigured(userInfo, authSettings);
    return {loggedIn: false, isSSO: hasSSO};
}

function getBaseHref(): string {
    return document.querySelector('head > base')?.getAttribute('href')?.replace(/\/$/, '') || '';
}

async function isExpiredSSO() {
    try {
        const user = await services.users.get();
        const authSettings = await services.authService.settings();
        return isSSOConfigured(user, authSettings);
    } catch {
        return false;
    }
}

export class App extends React.Component<
    {},
    {popupProps: PopupProps; showVersionPanel: boolean; error: Error; navItems: NavItem[]; routes: Routes; authSettings: AuthSettings; sessionResolved: boolean}
> {
    public static getDerivedStateFromError(error: Error) {
        return {error};
    }

    private popupManager: PopupManager;
    private notificationsManager: NotificationsManager;
    private navigationManager: NavigationManager;
    private navItems: NavItem[];
    private routes: Routes;
    private popupPropsSubscription: Subscription;
    private unauthorizedSubscription: Subscription;

    constructor(props: {}) {
        super(props);
        this.state = {
            popupProps: null,
            error: null,
            showVersionPanel: false,
            navItems: [],
            routes: null,
            authSettings: null,
            sessionResolved: false
        };
        this.popupManager = new PopupManager();
        this.notificationsManager = new NotificationsManager();
        this.navigationManager = new NavigationManager(history);
        this.navItems = navItems;
        this.routes = routes;
        this.popupPropsSubscription = null;
        this.unauthorizedSubscription = null;
        services.extensions.addEventListener('systemLevel', this.onAddSystemLevelExtension.bind(this));
    }

    public componentDidMount() {
        this.popupPropsSubscription = this.popupManager.popupProps.subscribe(popupProps => this.setState({popupProps}));
        this.subscribeUnauthorized().then(subscription => {
            this.unauthorizedSubscription = subscription;
        });

        void this.bootstrapAppSession().catch((err: unknown) => {
            console.error('Failed to bootstrap app session', err);
            this.setState(prev => ({
                ...prev,
                error: err instanceof Error ? err : new Error(String(err)),
                sessionResolved: true
            }));
        });
    }

    private async bootstrapAppSession(): Promise<void> {
        const [authSettings, userInfoResult, versionInfo] = await Promise.all([services.authService.settings(), fetchUserInfoSafe(), versionLoader]);

        const {loggedIn, isSSO} = sessionFromBootstrap(userInfoResult, authSettings, versionInfo);

        const {trackingID, anonymizeUsers} = authSettings.googleAnalytics || {trackingID: '', anonymizeUsers: true};
        const {loggedIn: userLoggedIn, username} = userInfoResult;
        if (trackingID) {
            const ga = await import('react-ga');
            ga.initialize(trackingID);
            const trackPageView = () => {
                if (userLoggedIn && username) {
                    const userId = !anonymizeUsers ? username : hashCode(username).toString();
                    ga.set({userId});
                }
                ga.pageview(location.pathname + location.search);
            };
            trackPageView();
            history.listen(trackPageView);
        }
        if (authSettings.uiCssURL) {
            const link = document.createElement('link');
            link.href = authSettings.uiCssURL;
            link.rel = 'stylesheet';
            link.type = 'text/css';
            document.head.appendChild(link);
        }

        const pathname = history.location.pathname;
        const onLoginPage = pathname === '/login';

        if (!onLoginPage && !loggedIn) {
            if (isSSO) {
                window.location.href = `${getBaseHref()}/auth/login?return_url=${encodeURIComponent(location.href)}`;
                return;
            }
            history.replace(`/login?return_url=${encodeURIComponent(location.href)}`);
        }

        this.setState(prev => ({
            ...prev,
            navItems: this.navItems,
            routes: this.routes,
            authSettings,
            sessionResolved: true
        }));
    }

    public componentWillUnmount() {
        if (this.popupPropsSubscription) {
            this.popupPropsSubscription.unsubscribe();
        }
        if (this.unauthorizedSubscription) {
            this.unauthorizedSubscription.unsubscribe();
        }
    }

    public render() {
        if (this.state.error != null) {
            const stack = this.state.error.stack;
            const url = 'https://github.com/argoproj/argo-cd/issues/new?labels=bug&template=bug_report.md';

            return (
                <React.Fragment>
                    <p>Something went wrong!</p>
                    <p>
                        Consider <a href={url}>submitting an issue</a>.
                    </p>
                    <br />
                    <p>Stacktrace:</p>
                    <pre>{stack}</pre>
                </React.Fragment>
            );
        }

        if (!this.state.sessionResolved) {
            return (
                <React.Fragment>
                    <Helmet>
                        <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-32x32.png`} sizes='32x32' />
                        <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-16x16.png`} sizes='16x16' />
                    </Helmet>
                    <div style={{display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: '12px'}}>
                        <Spinner show={true} />
                        <span style={{color: '#6d7f8b'}}>Checking session…</span>
                    </div>
                </React.Fragment>
            );
        }

        const contextApis = {history, popup: this.popupManager, notifications: this.notificationsManager, navigation: this.navigationManager, baseHref: base};

        // argo-ui's AppContext requires a `router`, but this provider sits above <Router> so there is no
        // route match yet — supply the current location with an empty match. The only fields argo-ui actually
        // reads off this context are `apis` (DataLoader) and `router` (NavBar, unused in Argo CD).
        const appContext: AppContext = {
            history,
            apis: {popup: this.popupManager, notifications: this.notificationsManager},
            router: {history, route: {location: history.location, match: {params: {}, isExact: false, path: '', url: ''}}}
        };

        return (
            <React.Fragment>
                <Helmet>
                    <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-32x32.png`} sizes='32x32' />
                    <link rel='icon' type='image/png' href={`${base}assets/favicon/favicon-16x16.png`} sizes='16x16' />
                </Helmet>
                <PageContext.Provider value={{title: 'Argo CD'}}>
                    <Provider value={contextApis}>
                        {/*
                          argo-ui's class components (e.g. DataLoader) read context via the modern
                          `static contextType = AppContextReact`. Without this provider, their `this.context`
                          is undefined and any error path (e.g. DataLoader.handleError after a failed
                          save/update/delete) throws "this.appContext is undefined".
                        */}
                        <AppContextReact.Provider value={appContext}>
                            <DataLoader load={() => services.viewPreferences.getPreferences()}>
                                {pref => <ThemeWrapper theme={pref.theme}>{this.state.popupProps && <Popup {...this.state.popupProps} />}</ThemeWrapper>}
                            </DataLoader>
                            <AuthSettingsCtx.Provider value={this.state.authSettings}>
                                <Router history={history}>
                                    <Switch>
                                        <Redirect exact={true} path='/' to='/applications' />
                                        {Object.keys(this.routes).map(path => {
                                            const route = this.routes[path];
                                            return (
                                                <Route
                                                    key={path}
                                                    path={path}
                                                    render={routeProps =>
                                                        route.noLayout ? (
                                                            <div>
                                                                <route.component {...routeProps} />
                                                            </div>
                                                        ) : (
                                                            <DataLoader load={() => services.viewPreferences.getPreferences()}>
                                                                {pref => (
                                                                    <Layout onVersionClick={() => this.setState({showVersionPanel: true})} navItems={this.navItems} pref={pref}>
                                                                        <Banner>
                                                                            <route.component {...routeProps} />
                                                                        </Banner>
                                                                    </Layout>
                                                                )}
                                                            </DataLoader>
                                                        )
                                                    }
                                                />
                                            );
                                        })}
                                    </Switch>
                                </Router>
                            </AuthSettingsCtx.Provider>
                        </AppContextReact.Provider>
                    </Provider>
                </PageContext.Provider>
                <Notifications notifications={this.notificationsManager.notifications} />
                <VersionPanel version={versionLoader} isShown={this.state.showVersionPanel} onClose={() => this.setState({showVersionPanel: false})} />
            </React.Fragment>
        );
    }

    private async subscribeUnauthorized() {
        return requests.onError.subscribe(async err => {
            if (err.status === 401) {
                if (history.location.pathname.startsWith('/login')) {
                    return;
                }

                const isSSO = await isExpiredSSO();
                // location might change after async method call, so we need to check again.
                if (history.location.pathname.startsWith('/login')) {
                    return;
                }
                // Query for basehref and remove trailing /.
                // If basehref is the default `/` it will become an empty string.
                const basehref = document.querySelector('head > base').getAttribute('href').replace(/\/$/, '');
                if (isSSO) {
                    window.location.href = `${basehref}/auth/login?return_url=${encodeURIComponent(location.href)}`;
                } else {
                    history.push(`/login?return_url=${encodeURIComponent(location.href)}`);
                }
            }
        });
    }

    private onAddSystemLevelExtension(extension: SystemLevelExtension) {
        const extendedNavItems = this.navItems;
        const extendedRoutes = this.routes;
        extendedNavItems.push({
            title: extension.title,
            path: extension.path,
            iconClassName: `fa ${extension.icon}`
        });
        const component = () => (
            <>
                <Helmet>
                    <title>{extension.title} - Argo CD</title>
                </Helmet>
                <Page title={extension.title}>
                    <extension.component />
                </Page>
            </>
        );
        extendedRoutes[extension.path] = {
            component: component as React.ComponentType<React.ComponentProps<any>>
        };
        this.setState(prev => ({...prev, navItems: extendedNavItems, routes: extendedRoutes}));
    }
}
