import { faTv, faCartPlus, faTrash } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon as Icon } from '@fortawesome/react-fontawesome'
import { IHomebridgeUiFormHelper } from '@homebridge/plugin-ui-utils/dist/ui.interface'
import { createState, none, State, useState } from '@hookstate/core'
import { ComponentChildren } from 'preact'
import { useEffect } from 'preact/compat'
import { Alert, Button, Form } from 'react-bootstrap'

import { UserConfig } from '../accessory'
import { sleep, isValidIPv4, Abnormal, dupeChecker, isSame, prettyPrint } from '../helpers'
import { VieraAuth, VieraSpecs } from '../viera'

import {
  authLayout,
  commonFormLayout,
  commonSchema,
  tvAddressSchema,
  pinRequestSchema
} from './forms'
import { Header } from './imagery'
import { InitialState, PluginConfig, rawClone, Selected } from './state'

const globalState = createState(InitialState)

const enum actionType {
  create = 'added',
  update = 'changed',
  delete = 'deleted',
  none = 'unchanged'
}

const { homebridge } = window

const updateGlobalConfig = async (discover = false) => {
  const bareMinimal = new PluginConfig()
  const pluginConfig = await homebridge
    .getPluginConfig()
    .catch(() => [bareMinimal])
    .then((found): PluginConfig[] => {
      found.length === 0 && found.push(bareMinimal)
      return found as unknown as PluginConfig[]
    })
    .then((found) => {
      const [cfg] = found
      cfg.tvs ??= []
      return cfg
    })

  const abnormal = Abnormal(dupeChecker(pluginConfig.tvs))
  globalState.merge({ abnormal, killSwitch: abnormal, loading: false, pluginConfig })

  if (!abnormal && discover) {
    globalState.loading.set(true)
    const around = (await homebridge.request('/discover')) as string[]
    const found = around.filter((t) => !pluginConfig.tvs.some((e) => e.ipAddress === t))
    const fn = (ip: string): UserConfig => {
      return { hdmiInputs: [], ipAddress: ip }
    }
    if (found.length > 0) {
      const discovered = found.map((ip: string) => fn(ip))
      await homebridge.updatePluginConfig([new PluginConfig([...pluginConfig.tvs, ...discovered])])
      await homebridge.savePluginConfig()
      for (const ip of found)
        homebridge.toast.info(
          `A new Panasonic Viera TV was discovered at ${ip}, on your network, and added to your homebridge. Click it to finish its' setup.`
        )
      await updateGlobalConfig(false)
    } else globalState.loading.set(false)
  }
}

const updateHomebridgeConfig = async (ip: string, next: UserConfig[], type: actionType) => {
  if (type !== actionType.none) {
    await homebridge.updatePluginConfig([new PluginConfig([...next])])
    await homebridge.savePluginConfig()
    await updateGlobalConfig(false)
  }
  homebridge.toast.success(`${ip} ${type}.`)
}

// https://dev.to/bytebodger/constructors-in-functional-components-with-hooks-280m
const useSingleton = (callBack = () => void 0): void => {
  const hasBeenCalled = useState(false)
  if (hasBeenCalled.value) return
  callBack()
  hasBeenCalled.set(true)
}

const Body = () => {
  useSingleton(() => void (async (): Promise<void> => await updateGlobalConfig())())
  const state = useState(globalState)

  useEffect(
    () => (state.loading.value ? homebridge.showSpinner() : homebridge.hideSpinner()),
    [state.loading.value]
  )

  const request = async (path: string, body?: unknown) => {
    state.loading.set(true)
    return await homebridge.request(path, body).finally(() => state.loading.set(false))
  }

  const previousConfig = (ip: string): UserConfig | undefined =>
    state.pluginConfig.tvs.value.find((o) => o.ipAddress === ip)

  const backToMain = (form?: IHomebridgeUiFormHelper) => {
    if (form) form.end()
    state.merge({ frontPage: true, selected: none })
  }

  const onEdition = async (raw?: string): Promise<void> => {
    const pair = (challenge: string) => {
      const pinForm = homebridge.createForm(pinRequestSchema, undefined, 'Next', 'Cancel')
      pinForm.onCancel(pinForm.end)
      pinForm.onSubmit(
        async (data) =>
          await request('/pair', { challenge, ip: tv.ipAddress, pin: data.pin })
            .then(async (auth: VieraAuth) => {
              const body = JSON.stringify({ ...tv, appId: auth.appId, encKey: auth.key })
              const specs: VieraSpecs = await request('/specs', body)
              return { auth, specs }
            })
            // eslint-disable-next-line promise/always-return
            .then((payload) => {
              const config = { ...tv, appId: payload.auth.appId, encKey: payload.auth.key }
              state.selected.merge({ config, onHold: false, reachable: true, specs: payload.specs })
            })
            .catch(() => {
              homebridge.toast.error('Wrong PIN...', tv.ipAddress)
              backToMain()
            })
            .finally(pinForm.end)
      )
    }

    if (!raw) {
      state.frontPage.set(false)
      const tvForm = homebridge.createForm(tvAddressSchema, undefined, 'Next', 'Cancel')
      tvForm.onCancel(() => backToMain(tvForm))

      tvForm.onSubmit(async (data) => {
        if (isValidIPv4(data.ipAddress)) {
          if (previousConfig(data.ipAddress))
            homebridge.toast.error('Trying to add an already configured TV set!', data.ipAddress)
          else {
            tvForm.end()
            const config = { hdmiInputs: [], ipAddress: data.ipAddress }
            state.selected.merge({ config, onHold: true })
          }
        } else homebridge.toast.error('Please insert a valid IP address...', data.ipAddress)
      })
    } else
      state.batch((s) => {
        s.selected.merge({ config: JSON.parse(raw), onHold: true }), s.frontPage.set(false)
      })

    while (!state.selected.value?.config) await sleep(250)
    const tv = state.selected.value.config
    await request('/ping', tv.ipAddress).then(async (reachable: boolean) => {
      /* eslint-disable promise/no-nesting*/
      if (!reachable) return state.selected.merge({ onHold: false, reachable })
      return await request('/specs', JSON.stringify(tv))
        .then((specs) => state.selected.merge({ onHold: false, reachable, specs }))
        .catch(async () => await request('/pin', tv.ipAddress).then((challenge) => pair(challenge)))
    })
  }

  const onDeletion = (raw: string) =>
    state.batch((s) => {
      s.frontPage.set(false), s.selected.merge({ config: JSON.parse(raw), onHold: false })
    })

  const FrontPage = () => {
    const flip = () => !state.abnormal.value && state.killSwitch.set((k) => !k)
    const label = `${state.killSwitch.value ? 'deletion' : 'edition'} mode`
    const doIt = (tv: string) => (state.killSwitch.value ? onDeletion(tv) : onEdition(tv))
    const KillBox = () =>
      state.pluginConfig.value.tvs.length === 0 ? (
        <></>
      ) : state.abnormal.value ? (
        <Alert variant="warning" className="d-flex justify-content-center mt-3 mb-5">
          <b>more than one TV with same IP address found: please delete the bogus ones!</b>
        </Alert>
      ) : (
        <Form className="d-flex justify-content-end mt-3 mb-5">
          <Form.Switch onChange={flip} id="kS" label={label} checked={state.killSwitch.value} />
        </Form>
      )
    const style = { height: '4em', width: '10em' }
    const AddNew = () =>
      state.killSwitch.value ? (
        <></>
      ) : (
        <div className="d-flex justify-content-center mt-3 mb-5">
          <Button
            className="my-4"
            variant="primary"
            onClick={async () => await onEdition()}
            style={style}
          >
            <Icon fixedWidth size="sm" icon={faTv} /> <br />
            <Icon fixedWidth size="lg" icon={faCartPlus} />
          </Button>
        </div>
      )
    const Available = () => {
      const variant = state.killSwitch.value ? 'danger' : 'info'
      const onClick = (tv: UserConfig) => doIt(JSON.stringify(tv))
      const tvs = state.pluginConfig.value.tvs.map((tv, index) => (
        <Button variant={variant} style={style} key={index} onClick={() => onClick(tv)}>
          <Icon fixedWidth size="lg" icon={state.killSwitch.value ? faTrash : faTv} />
          <br /> {tv.ipAddress}
        </Button>
      ))
      return <>{tvs}</>
    }

    return (
      <section className="mh-100">
        <KillBox /> <Available /> <AddNew />
      </section>
    )
  }

  const Results = (props: { selected: State<Selected> | undefined }) => {
    if (!props.selected || props.selected.onHold.value) return <></>

    const Offline = (props: { selected: State<Selected> }) => (
      <Alert variant="danger" className="mt-3">
        <Alert.Heading>
          The Viera TV at <b>{props.selected.config.ipAddress.value}</b> could not be edited.
        </Alert.Heading>
        <hr />
        <p className="mb-2">
          Please, do make sure that it is <b>turned on</b> and <b>connected to the network</b>, and
          then try again.
        </p>
        <div className="mt-4 w-75 mx-auto">
          <p className="text-left ">
            Also, <b>if you haven't done it already</b>...
          </p>
          <p className="text-left">
            ...on your TV go to <b>Menu / Network / TV Remote App Settings</b> and make sure that
            the following settings are <b>all</b> turned <b>ON</b>:
            <ul className="mt-2">
              <li>
                <b>TV Remote</b>
              </li>
              <li>
                <b>Powered On by Apps</b>
              </li>
              <li>
                <b>Networked Standby</b>
              </li>
            </ul>
          </p>
        </div>
        <div className="d-flex justify-content-end mt-5">
          <Button onClick={() => backToMain()} variant="primary">
            OK
          </Button>
        </div>
      </Alert>
    )

    const ConfirmDeletion = (props: { selected: State<Selected> }) => {
      const { ipAddress } = props.selected.config.value
      const nxt = rawClone(state.pluginConfig.value.tvs.filter((o) => o.ipAddress !== ipAddress))
      const dropIt = async () =>
        await updateHomebridgeConfig(ipAddress, nxt, actionType.delete).then(() => backToMain())

      return (
        <Alert variant="danger" className="mt-3">
          <Alert.Heading>
            The Viera TV at <b>{ipAddress}</b> is about to be deleted from this Homebridge.
          </Alert.Heading>
          <hr />
          <div className="d-flex justify-content-center">
            <div className="w-75">
              <p className="mb-2">Please, make sure you know what you are doing...</p>
              <hr />
              <pre class="text-monospace text-left bg-light p-2">
                {prettyPrint(props.selected.config.value)}
              </pre>
              <hr />
            </div>
          </div>
          <div className="d-flex justify-content-end mt-1">
            <Button onClick={() => backToMain()} variant="primary">
              Cancel
            </Button>
            <Button onClick={() => dropIt()} variant="danger">
              Delete
            </Button>
          </div>
        </Alert>
      )
    }

    const Editor = (props: { selected: State<Selected> }) => {
      if (props.selected.specs.ornull?.requiresEncryption.value)
        commonFormLayout.splice(1, 0, authLayout)

      const schema = { layout: commonFormLayout, schema: commonSchema }
      const data = rawClone(props.selected.config.value)
      const tvform = homebridge.createForm(schema, data, 'Submit', 'Cancel')
      tvform.onCancel(() => backToMain(tvform))
      tvform.onSubmit(async (submited) => {
        const queued = submited as UserConfig
        state.loading.set(true)
        backToMain(tvform)
        const before = previousConfig(queued.ipAddress)
        let [others, type] = [[] as UserConfig[], actionType.none]

        if (!isSame(before, queued)) {
          const modded = before !== undefined
          const { tvs } = state.pluginConfig.value
          others = modded ? rawClone(tvs.filter((v) => v.ipAddress != queued.ipAddress)) : []
          type = modded ? actionType.update : actionType.create
        }
        await updateHomebridgeConfig(queued.ipAddress, [...others, queued], type).finally(() =>
          state.loading.set(false)
        )
      })
      return <></>
    }

    if (state.killSwitch.value) return <ConfirmDeletion selected={props.selected} />
    if (props.selected.reachable.value) return <Editor selected={props.selected} />
    return <Offline selected={props.selected} />
  }

  return state.frontPage.value ? <FrontPage /> : <Results selected={state.selected.ornull} />
}

const Template = (props: { children: ComponentChildren }) => (
  <main className="align-items-center text-center align-content-center">{props.children}</main>
)

const VieraConfigUI = () => (
  <Template>
    <Header />
    <Body />
  </Template>
)

export default VieraConfigUI