import {html, LitElement} from 'lit'
import {customElement, property} from 'lit/decorators.js'
import {DataPlane} from '../DataPlane'
import {Task} from '@lit/task'
import {LayoutSpec} from '../layout/Layout'
import {CobaltAction, CobaltActionResult, LoadAction} from '../types'

export type HostSettings = {
    parentContext?: Record<string, any>,
    layoutChildren: LayoutSpec[],
    loadActions?: LoadAction[],
}

@customElement('cobalt-host')
export class Host extends LitElement {
    @property()
    settingsendpoint?: string

    @property()
    layoutname?: string

    @property({ attribute: false })
    plane?: DataPlane

    @property()
    paramsSourceName: string = 'params'

    @property()
    initialParamsSourceName?: string

    @property({ attribute: false })
    initialParams?: Record<string, any>

    @property()
    initialparamsjson?: string

    private settings?: HostSettings

    private renderTask = new Task(this, {
        args: () => [this.settingsendpoint, this.layoutname, this.plane, this.initialParams, this.initialparamsjson],
        task: async ([settingsEndpoint, layoutName,,], options) => {
            if ( this.settings ) {
                return this.settings
            }

            const plane = this.getDataPlane()
            plane.ensureSource(this.paramsSourceName)

            const params = this.getParams()
            await plane.getAll(this.paramsSourceName)!.next(params)

            const searchQuery = new URLSearchParams({
                layoutname: `${layoutName || ''}`,
                params: JSON.stringify(params),
            })

            const settingsUrl = `${settingsEndpoint}?${searchQuery}`
            const response = await fetch(settingsUrl)
            const json: any = await response.json()

            if ( !json.success || !json.data?.layoutChildren ) {
                return
            }

            this.settings = json.data
            await this.runLoadActions()
            return this.settings
        }
    })

    override render() {
        return this.renderTask.render({
            pending: () => html`...`,
            complete: (settings?: HostSettings) => {
                if ( !settings ) {
                    return html`...`
                }

                return html`
                    <cobalt-layout
                        .plane=${this.getDataPlane()}
                        .parentContext=${settings.parentContext || {}}
                        .layoutChildren=${settings.layoutChildren}
                    ></cobalt-layout>
                `
            },
            error: e => html`Error: ${e}`,
        })
    }

    private getParams(): Record<string, any> {
        let params: Record<string, any> = {}

        // Merge params from the data plane, if we have any:
        if ( this.initialParamsSourceName ) {
            const planeParams = this.getDataPlane()
                .getAll(this.initialParamsSourceName)
                ?.value()

            if ( planeParams ) {
                params = {...params, ...planeParams}
            }
        }

        // Merge params passed in via JSON, if we have any:
        if ( this.initialparamsjson ) {
            const jsonParams = JSON.parse(this.initialparamsjson)
            params = {...params, ...jsonParams}
        }

        // Merge params passed in directly, if we have any:
        if ( this.initialParams ) {
            params = {...params, ...this.initialParams}
        }

        return params
    }

    private dp?: DataPlane
    private getDataPlane(): DataPlane {
        return this.plane || this.dp || (this.dp = new DataPlane)
    }

    private async runLoadActions(): Promise<void> {
        if ( !this.settings?.loadActions ) {
            return
        }

        await Promise.all(
            this.settings
                .loadActions
                .map(load => this.runLoadAction(load))
        )
    }

    private async runLoadAction(load: LoadAction): Promise<void> {
        const result = await this.runAction(load.action)
        if ( !result.success ) {
            console.error('Load action failed!', result)
            return  // fixme: handle this better
        }

        console.log(`setting load data for ${load.target.sourceName}->${load.target.fieldName || '@'}`, result)

        this.getDataPlane().ensureSource(load.target.sourceName)

        const target = load.target.fieldName
            ? this.getDataPlane().getField(load.target.sourceName, load.target.fieldName)
            : this.getDataPlane().getAll(load.target.sourceName)

        await target?.next(result.result)
    }

    private async runAction(action: CobaltAction): Promise<CobaltActionResult> {
        try {
            const params = await this.gatherActionParams(action)

            let result: any
            if ( action.type === 'route' ) {
                result = await this.runRouteAction(action, params)
            }

            if ( action.successKey && !result[action.successKey] ) {
                return {
                    success: false,
                    message: result.message || undefined,
                    error: result,
                }
            }

            return {
                success: true,
                message: result.message || undefined,
                result: action.resultKey ? result[action.resultKey] : result,
            }
        } catch (e) {
            return {
                success: false,
                error: e,
            }
        }
    }

    private async runRouteAction(action: CobaltAction & { type: 'route' }, params: Record<string, unknown>): Promise<any> {
        let body: undefined|string = undefined
        let route = action.route
        if ( action.method === 'GET' || action.method === 'HEAD' ) {
            route = `${route}?${new URLSearchParams({ params: JSON.stringify(params) })}`
        } else {
            body = JSON.stringify({ params })
        }

        const response = await fetch(route, {
            method: action.method,
            body,
        })

        return await response.json()
    }

    private async gatherActionParams(action: CobaltAction): Promise<Record<string, unknown>> {
        let params: Record<string, unknown> = {}

        if ( action.data ) {
            params = {...params, ...action.data}
        }

        if ( action.gather ) {
            const plane = this.getDataPlane()
            for ( const key in action.gather ) {
                const ref = action.gather[key]
                if ( !ref ) {
                    continue
                }

                if ( ref.fieldName ) {
                    params[key] = plane.getField(ref.sourceName, ref.fieldName)?.value()
                } else {
                    params[key] = plane.getAll(ref.sourceName)?.value()
                }
            }
        }

        return params
    }
}
