/**
 * ConfigPage — config editor (M2-T08, M5-T01B).
 *
 * Behaviours:
 * 1. Load config: GET /api/config → render sections as Accordion items (M5-T01B).
 *    - Non-secret fields show their value.
 *    - Secret fields render as empty PasswordInput (never show a masked value).
 * 2. Save config: PUT /api/config with full-field submission semantics.
 *    - All non-secret fields are ALWAYS included (to avoid backend zeroing absent fields).
 *    - Secret fields are included ONLY when the user typed a new (non-empty) value.
 *    - On success: show a success notice and refetch config.
 *    - On ApiError 422: show an error notice, nothing was written.
 * 3. SMTP test button: POST /api/config/smtp/test.
 *    - Tri-state: success / config-error / failed.
 *    - Errors read `err.body.result` from ApiError.
 *
 * Layout (M5-T01B): each config section = one Accordion.Item. The first section
 * is open by default. Future panels (e.g. T12 Home Assistant Expose) should be
 * appended as additional Accordion.Items after the config sections.
 */

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import {
  Accordion,
  Container,
  Title,
  Text,
  TextInput,
  PasswordInput,
  Button,
  Alert,
  Stack,
  Group,
  Divider,
  Loader,
  Center,
  Badge,
  Switch,
} from '@mantine/core'
import apiClient, { ApiError } from '../api/client'
import type { components } from '../api/schema.d.ts'
import { ExposeSettings } from '../components/ExposeSettings'
import { TotpSettings } from './TotpSettings'

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

type ConfigField = components['schemas']['ConfigField']
type ConfigSection = components['schemas']['ConfigSection']
type TibberTestPriceSchema = components['schemas']['TibberTestPriceSchema']

/** SMTP test result tri-state. */
type SmtpResult =
  | { kind: 'success'; message: string }
  | { kind: 'config-error'; message: string }
  | { kind: 'failed'; message: string }
  | null

/** MQTT test result tri-state. */
type MqttResult =
  | { kind: 'success'; message: string }
  | { kind: 'config-error'; message: string }
  | { kind: 'failed'; message: string }
  | null

/** Tibber test result tri-state. */
type TibberResult =
  | { kind: 'success'; message: string; price?: TibberTestPriceSchema | null }
  | { kind: 'config-error'; message: string }
  | { kind: 'failed'; message: string }
  | null

// ---------------------------------------------------------------------------
// Hook: load config
// ---------------------------------------------------------------------------

function useConfig() {
  return useQuery({
    queryKey: ['config'],
    queryFn: async () => {
      const res = await apiClient.GET('/api/config')
      return res.data
    },
  })
}

// ---------------------------------------------------------------------------
// Helper: build updates map for PUT /api/config
//
// Full-field submission semantics (§6):
//   - Non-secret fields: ALWAYS include current value (even if unchanged) so
//     the backend does not zero out absent fields.
//   - Secret fields: include ONLY when the user typed a non-empty value.
//     Blank secret = keep old value; sending blank would also keep it per
//     backend semantics, but we omit it to be explicit and avoid confusion.
// ---------------------------------------------------------------------------

function buildUpdates(
  sections: ConfigSection[],
  localValues: Record<string, string>,
): Record<string, string> {
  const updates: Record<string, string> = {}

  for (const section of sections) {
    for (const field of section.fields) {
      const localVal = localValues[field.env_name] ?? ''
      if (field.secret) {
        // Only include secret if the user typed something (non-empty).
        if (localVal !== '') {
          updates[field.env_name] = localVal
        }
        // blank secret → omit → backend keeps the existing stored value
      } else {
        // Non-secret: always include current local value.
        updates[field.env_name] = localVal
      }
    }
  }

  return updates
}

// ---------------------------------------------------------------------------
// ConfigFieldInput — renders a single config field
// ---------------------------------------------------------------------------

interface ConfigFieldInputProps {
  field: ConfigField
  value: string
  onChange: (envName: string, value: string) => void
}

function ConfigFieldInput({ field, value, onChange }: ConfigFieldInputProps) {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    onChange(field.env_name, e.currentTarget.value)
  }

  if (field.secret) {
    return (
      <PasswordInput
        label={field.label}
        placeholder={field.configured ? '(configured — leave blank to keep)' : 'Enter value'}
        value={value}
        onChange={handleChange}
        data-testid={`field-secret-${field.env_name}`}
        autoComplete="off"
      />
    )
  }

  if (field.input_type === 'checkbox') {
    return (
      <Switch
        label={field.label}
        checked={(value ?? '').toLowerCase() === 'true'}
        onChange={(e) => onChange(field.env_name, e.currentTarget.checked ? 'true' : 'false')}
        data-testid={`field-${field.env_name}`}
      />
    )
  }

  if (field.input_type === 'number') {
    return (
      <TextInput
        label={field.label}
        type="number"
        value={value}
        onChange={handleChange}
        data-testid={`field-${field.env_name}`}
      />
    )
  }

  return (
    <TextInput
      label={field.label}
      type={field.input_type === 'email' ? 'email' : 'text'}
      value={value}
      onChange={handleChange}
      data-testid={`field-${field.env_name}`}
    />
  )
}

// ---------------------------------------------------------------------------
// ConfigSectionFields — the fields inside one accordion panel
// ---------------------------------------------------------------------------

interface ConfigSectionFieldsProps {
  section: ConfigSection
  localValues: Record<string, string>
  onChange: (envName: string, value: string) => void
}

function ConfigSectionFields({ section, localValues, onChange }: ConfigSectionFieldsProps) {
  return (
    <Stack gap="sm">
      {section.fields.map((field) => (
        <ConfigFieldInput
          key={field.env_name}
          field={field}
          value={localValues[field.env_name] ?? ''}
          onChange={onChange}
        />
      ))}
    </Stack>
  )
}

// ---------------------------------------------------------------------------
// SmtpTestButton — sends POST /api/config/smtp/test and displays tri-state result
// ---------------------------------------------------------------------------

interface SmtpTestButtonProps {
  smtpResult: SmtpResult
  setSmtpResult: (r: SmtpResult) => void
}

function SmtpTestButton({ smtpResult, setSmtpResult }: SmtpTestButtonProps) {
  const [testing, setTesting] = useState(false)

  async function handleTest() {
    setSmtpResult(null)
    setTesting(true)
    try {
      const res = await apiClient.POST('/api/config/smtp/test')
      if (res.data) {
        setSmtpResult({ kind: 'success', message: res.data.message })
      }
    } catch (err) {
      if (err instanceof ApiError) {
        const body = err.body as { result?: string; message?: string } | null
        const result = body?.result
        const message = body?.message ?? 'Unknown error'
        if (result === 'config-error') {
          setSmtpResult({ kind: 'config-error', message })
        } else {
          // result === 'failed' or any other error
          setSmtpResult({ kind: 'failed', message })
        }
      } else {
        setSmtpResult({ kind: 'failed', message: 'Unexpected error sending test email.' })
      }
    } finally {
      setTesting(false)
    }
  }

  return (
    <Stack gap="xs">
      <Button
        variant="outline"
        onClick={handleTest}
        loading={testing}
        data-testid="smtp-test-button"
      >
        Send Test Email
      </Button>

      {smtpResult?.kind === 'success' && (
        <Alert color="green" data-testid="smtp-result-success">
          Test email sent successfully. {smtpResult.message}
        </Alert>
      )}
      {smtpResult?.kind === 'config-error' && (
        <Alert color="orange" data-testid="smtp-result-config-error">
          SMTP configuration error — check your SMTP settings. {smtpResult.message}
        </Alert>
      )}
      {smtpResult?.kind === 'failed' && (
        <Alert color="red" data-testid="smtp-result-failed">
          Test email send failed. {smtpResult.message}
        </Alert>
      )}
    </Stack>
  )
}

// ---------------------------------------------------------------------------
// MqttTestButton — sends POST /api/config/mqtt/test and displays tri-state result
// ---------------------------------------------------------------------------

interface MqttTestButtonProps {
  mqttResult: MqttResult
  setMqttResult: (r: MqttResult) => void
}

function MqttTestButton({ mqttResult, setMqttResult }: MqttTestButtonProps) {
  const [testing, setTesting] = useState(false)

  async function handleTest() {
    setMqttResult(null)
    setTesting(true)
    try {
      const res = await apiClient.POST('/api/config/mqtt/test')
      if (res.data) {
        setMqttResult({ kind: 'success', message: res.data.message })
      }
    } catch (err) {
      if (err instanceof ApiError) {
        const body = err.body as { result?: string; message?: string } | null
        const result = body?.result
        const message = body?.message ?? 'Unknown error'
        if (result === 'config-error') {
          setMqttResult({ kind: 'config-error', message })
        } else {
          setMqttResult({ kind: 'failed', message })
        }
      } else {
        setMqttResult({ kind: 'failed', message: 'Unexpected error sending test message.' })
      }
    } finally {
      setTesting(false)
    }
  }

  return (
    <Stack gap="xs">
      <Button
        type="button"
        variant="outline"
        onClick={handleTest}
        loading={testing}
        data-testid="mqtt-test-button"
      >
        Send Test Message
      </Button>

      {mqttResult?.kind === 'success' && (
        <Alert color="green" data-testid="mqtt-result-success">
          MQTT test message sent successfully. {mqttResult.message}
        </Alert>
      )}
      {mqttResult?.kind === 'config-error' && (
        <Alert color="orange" data-testid="mqtt-result-config-error">
          MQTT configuration error — check your MQTT settings. {mqttResult.message}
        </Alert>
      )}
      {mqttResult?.kind === 'failed' && (
        <Alert color="red" data-testid="mqtt-result-failed">
          MQTT test failed. {mqttResult.message}
        </Alert>
      )}
    </Stack>
  )
}

// ---------------------------------------------------------------------------
// TibberTestButton — sends POST /api/energy/tibber/test and displays tri-state result
// ---------------------------------------------------------------------------

interface TibberTestButtonProps {
  tibberResult: TibberResult
  setTibberResult: (r: TibberResult) => void
}

function TibberTestButton({ tibberResult, setTibberResult }: TibberTestButtonProps) {
  const [testing, setTesting] = useState(false)

  async function handleTest() {
    setTibberResult(null)
    setTesting(true)
    try {
      const res = await apiClient.POST('/api/energy/tibber/test')
      if (res.data) {
        setTibberResult({
          kind: 'success',
          message: res.data.message,
          price: res.data.price,
        })
      }
    } catch (err) {
      if (err instanceof ApiError) {
        const body = err.body as { result?: string; message?: string } | null
        const result = body?.result
        const message = body?.message ?? 'Unknown error'
        if (result === 'config-error') {
          setTibberResult({ kind: 'config-error', message })
        } else {
          setTibberResult({ kind: 'failed', message })
        }
      } else {
        setTibberResult({ kind: 'failed', message: 'Unexpected error during Tibber test.' })
      }
    } finally {
      setTesting(false)
    }
  }

  return (
    <Stack gap="xs">
      <Button
        type="button"
        variant="outline"
        onClick={handleTest}
        loading={testing}
        data-testid="tibber-test-button"
      >
        Test Tibber Connection
      </Button>

      {tibberResult?.kind === 'success' && (
        <Alert color="green" data-testid="tibber-result-success">
          Tibber connection successful. {tibberResult.message}
          {tibberResult.price && (
            <Text size="xs" mt={4}>
              Current price: {tibberResult.price.total} {tibberResult.price.currency}/kWh
              {tibberResult.price.level ? ` (${tibberResult.price.level})` : ''}
            </Text>
          )}
        </Alert>
      )}
      {tibberResult?.kind === 'config-error' && (
        <Alert color="orange" data-testid="tibber-result-config-error">
          Tibber configuration error — check your Tibber API token. {tibberResult.message}
        </Alert>
      )}
      {tibberResult?.kind === 'failed' && (
        <Alert color="red" data-testid="tibber-result-failed">
          Tibber test failed. {tibberResult.message}
        </Alert>
      )}
    </Stack>
  )
}

// ---------------------------------------------------------------------------
// ConfigPage — main component
// ---------------------------------------------------------------------------

export function ConfigPage() {
  const queryClient = useQueryClient()
  const { data, isLoading, isError } = useConfig()

  // Local field values — mirrors the loaded config but allows user edits.
  // Secret fields always start as empty string (never display masked values).
  const [localValues, setLocalValues] = useState<Record<string, string>>({})
  const [valuesInitialized, setValuesInitialized] = useState(false)

  // Initialise local state once when data arrives (or re-arrives after refetch).
  if (data && !valuesInitialized) {
    const initial: Record<string, string> = {}
    for (const section of data.sections) {
      for (const field of section.fields) {
        // Secret fields start empty (never display the masked/empty backend value).
        initial[field.env_name] = field.secret ? '' : (field.value ?? '')
      }
    }
    setLocalValues(initial)
    setValuesInitialized(true)
  }

  // Save notice state
  const [saveStatus, setSaveStatus] = useState<'success' | 'error' | null>(null)

  // SMTP test tri-state
  const [smtpResult, setSmtpResult] = useState<SmtpResult>(null)

  // MQTT test tri-state
  const [mqttResult, setMqttResult] = useState<MqttResult>(null)

  // Tibber test tri-state
  const [tibberResult, setTibberResult] = useState<TibberResult>(null)

  function handleChange(envName: string, value: string) {
    setLocalValues((prev) => ({ ...prev, [envName]: value }))
    setSaveStatus(null)
  }

  const saveMutation = useMutation({
    mutationFn: async () => {
      if (!data) return
      const updates = buildUpdates(data.sections, localValues)
      await apiClient.PUT('/api/config', { body: { updates } })
    },
  })

  async function handleSave(e: React.FormEvent) {
    e.preventDefault()
    setSaveStatus(null)
    try {
      await saveMutation.mutateAsync()
      setSaveStatus('success')
      // Refetch config so the page reflects the saved state.
      await queryClient.invalidateQueries({ queryKey: ['config'] })
      // After refetch, reset initialised flag so local state rebuilds from fresh data.
      setValuesInitialized(false)
    } catch {
      setSaveStatus('error')
    }
  }

  // ---------------------------------------------------------------------------
  // Render states
  // ---------------------------------------------------------------------------

  if (isLoading) {
    return (
      <Center pt="xl">
        <Loader data-testid="config-loading" />
      </Center>
    )
  }

  if (isError || !data) {
    return (
      <Container pt="xl">
        <Alert color="red" data-testid="config-load-error">
          Failed to load configuration. Please refresh the page.
        </Alert>
      </Container>
    )
  }

  // Detect if there is an SMTP section (to show the test button).
  const hasSmtpSection = data.sections.some((s) =>
    s.name.toLowerCase().includes('smtp') || s.name.toLowerCase().includes('email'),
  )

  // Detect if there is an MQTT section (to show the MQTT test button).
  const hasMqttSection = data.sections.some((s) => s.name.toLowerCase() === 'mqtt')

  // Detect if there is a Tibber section (to show the Tibber test button).
  const hasTibberSection = data.sections.some((s) => s.name.toLowerCase() === 'tibber')

  // Default: open the first section so users immediately see content.
  const defaultAccordionValue = data.sections[0]?.name ?? null

  return (
    <Container size="md" pt="xl" pb="xl" data-testid="config-page">
      <Group justify="space-between" mb="lg" wrap="nowrap">
        <Title order={2}>Configuration</Title>
        <Badge variant="outline" color="gray" size="sm">
          {data.sections.length} section{data.sections.length !== 1 ? 's' : ''}
        </Badge>
      </Group>

      <form onSubmit={handleSave} data-testid="config-form">
        <Stack gap="lg">
          {/* M5-T01B: each config section is one Accordion.Item.
              Future panels (e.g. T12 Home Assistant Expose) should be appended
              as additional Accordion.Items here, after the config sections. */}
          <Accordion
            defaultValue={defaultAccordionValue}
            variant="separated"
            radius="md"
            data-testid="config-accordion"
          >
            {data.sections.map((section) => (
              <Accordion.Item key={section.name} value={section.name}>
                <Accordion.Control data-testid={`accordion-control-${section.name}`}>
                  <Text fw={500}>{section.name}</Text>
                </Accordion.Control>
                <Accordion.Panel>
                  <ConfigSectionFields
                    section={section}
                    localValues={localValues}
                    onChange={handleChange}
                  />
                </Accordion.Panel>
              </Accordion.Item>
            ))}

            {/* M5-fix2: Home Assistant Expose panel merged into the config accordion,
                above Save/Test buttons.  All interactive elements inside ExposeSettings
                use type="button" so they do not trigger the config form submit. */}
            <Accordion.Item value="expose" data-testid="expose-accordion-item">
              <Accordion.Control data-testid="accordion-control-expose">
                <Text fw={500}>Home Assistant Expose</Text>
              </Accordion.Control>
              <Accordion.Panel>
                <ExposeSettings />
              </Accordion.Panel>
            </Accordion.Item>
          </Accordion>

          <Divider />

          {saveStatus === 'success' && (
            <Alert color="green" data-testid="save-success">
              Configuration saved successfully.
            </Alert>
          )}
          {saveStatus === 'error' && (
            <Alert color="red" data-testid="save-error">
              Failed to save configuration. Please check the values and try again.
            </Alert>
          )}

          <Group justify="space-between" align="center" wrap="wrap" gap="sm">
            <Button
              type="submit"
              loading={saveMutation.isPending}
              data-testid="config-save-button"
            >
              Save Configuration
            </Button>

            <Group gap="sm" wrap="wrap">
              {hasSmtpSection && (
                <SmtpTestButton smtpResult={smtpResult} setSmtpResult={setSmtpResult} />
              )}
              {hasMqttSection && (
                <MqttTestButton mqttResult={mqttResult} setMqttResult={setMqttResult} />
              )}
              {hasTibberSection && (
                <TibberTestButton tibberResult={tibberResult} setTibberResult={setTibberResult} />
              )}
            </Group>
          </Group>
        </Stack>
      </form>

      {!hasSmtpSection && (
        <Stack mt="md">
          <Text c="dimmed" size="sm">
            Configure SMTP settings to enable email notifications.
          </Text>
        </Stack>
      )}

      {/* TOTP two-factor auth management */}
      <Stack mt="xl">
        <TotpSettings />
      </Stack>
    </Container>
  )
}
