import { Box, Button, Card, Divider, List, ListItem, Typography } from "@mui/material";
import Grid from '@mui/material/Grid2';
import { Component, Device, Interaction, User } from "models";
import { getContextKeys, getContextTypes } from "pbHelpers/Context";
import { useDeviceAPI, useGlobalState, useSnackbar } from "providers";
import { useDeviceStatusStreams } from "hooks/useDeviceStatusStreams";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useLocation, useParams } from "react-router-dom";
import PageContainer from "./PageContainer";
import { useHTTP, useMobile } from "hooks";
import LoadingScreen from "app/LoadingScreen";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import DeviceActions from "device/DeviceActions";
import { pond, quack } from "protobuf-ts/pond";
import { cloneDeep } from "lodash";
import DeviceOverview from "device/DeviceOverview";
import { DeviceAvailabilityMap, FindAvailablePositions, OffsetAvailabilityMap } from "pbHelpers/DeviceAvailability";
import ComponentCard from "component/ComponentCard";
import { sameComponentID, sortComponents } from "pbHelpers/Component";
import { isController } from "pbHelpers/ComponentType";
import { or } from "utils";
import ComponentDiagnostics from "component/ComponentDiagnostics";
import DeviceWizard from "device/DeviceWizard";
import DeviceScannedComponents from "device/autoDetect/DeviceScannedComponents";
import { useWebSocket } from "hooks/useWebSocket";

type TimestampedReading = { timestamp?: string };

function getTimestampMillis(timestamp?: string): number | undefined {
  if (!timestamp) return undefined;
  const parsed = Date.parse(timestamp);
  return Number.isNaN(parsed) ? undefined : parsed;
}

function pickNewestReading<T extends TimestampedReading>(
  current: T | null | undefined,
  incoming: T | null | undefined
): T | null | undefined {
  const currentTs = getTimestampMillis(current?.timestamp);
  const incomingTs = getTimestampMillis(incoming?.timestamp);
  if (incomingTs === undefined) return current ?? incoming;
  if (currentTs === undefined) return incoming ?? current;
  return incomingTs >= currentTs ? incoming : current;
}

export interface DevicePageData {
  device: Device;
  components: Component[];
  interactions: Interaction[];
  permissions: pond.Permission[];
  preferences: pond.DevicePreferences;
  // componentPreferences: Map<string, pond.DeviceComponentPreferences>
}

export default function DevicePage() {
  const deviceAPI = useDeviceAPI()
  const snackbar = useSnackbar()
  const isMobile = useMobile()
  const deviceID = useParams<{ deviceID: string }>()?.deviceID ?? "";
  const groupID = useParams<{ groupID: string }>()?.groupID ?? "";
  const { state, pathname: locationPathname } = useLocation();
  const [{ as, team, user }] = useGlobalState()
  // console.log(state)
  const [device, setDevice] = useState<Device>(state?.device ? Device.create(state.device) : Device.create())
  const [loading, setLoading] = useState(false)
  const [permissions, setPermissions] = useState<pond.Permission[]>([])
  const [preferences, setPreferences] = useState<pond.DevicePreferences>(pond.DevicePreferences.create())
  // const [tags, setTags] = useState<pond.Tag[]>([]);
  const [interactions, setInteractions] = useState<Interaction[]>([]);
  const [prefsMap, setPrefsMap] = useState<Map<string, pond.DeviceComponentPreferences>>(new Map());

  const [cellularUsage, _setCellularUsage] = useState<number>(0);
  const [cellularStatus, _setCellularStatus] = useState<string>("");

  const [availablePositions, setAvailablePositions] = useState<DeviceAvailabilityMap>(new Map());
  const [availableOffsets, setAvailableOffsets] = useState<OffsetAvailabilityMap>(new Map());
  const [displayOrder, setDisplayOrder] = useState<string[]>([]);

  const [components, setComponents] = useState<Map<string, Component>>(new Map());
  const [diagnosticComponents, setDiagnosticComponents] = useState<Component[]>([]);
  const [scannedAddresses, setScannedAddresses] = useState<pond.ComponentAddressMap | undefined>(undefined)
  const [scanInProgress, setScanInProgress] = useState(false)
  // const [components, setComponents] = useState<Component[]>([]);

  const [addComponentManualDialogOpen, setAddComponentManualDialogOpen] = useState(false)

  // const [devicePageData, setDevicePageData] = useState(pond.GetDevicePageDataResponse.create())

  const { token } = useHTTP();

  const liveContextKeys = useMemo(() => getContextKeys(), [locationPathname]);
  const liveContextTypes = useMemo(() => getContextTypes(), [locationPathname]);
  /** Matches getDevicePageData: omit ?as= when first context type is "team". */
  const liveAs =
    as && !(liveContextTypes.length > 0 && liveContextTypes[0] === "team")
      ? as
      : undefined;

  const liveStatusDeviceIds = useMemo(
    () => (deviceID ? [deviceID] : []),
    [deviceID]
  );

  const handleLiveDeviceStatus = useCallback(
    (streamDeviceId: string, status: pond.DeviceStatus) => {
      setDevice((prev) => {
        if (prev.id().toString() !== streamDeviceId) return prev;
        const updated = Device.clone(prev);
        const incomingStatus = pond.DeviceStatus.fromObject(status);
        const currentStatus = prev.status ?? pond.DeviceStatus.create();

        incomingStatus.plenum = pickNewestReading(currentStatus.plenum, incomingStatus.plenum);
        incomingStatus.sen5x = pickNewestReading(currentStatus.sen5x, incomingStatus.sen5x);
        incomingStatus.co = pickNewestReading(currentStatus.co, incomingStatus.co);
        incomingStatus.co2 = pickNewestReading(currentStatus.co2, incomingStatus.co2);
        incomingStatus.no2 = pickNewestReading(currentStatus.no2, incomingStatus.no2);
        incomingStatus.o2 = pickNewestReading(currentStatus.o2, incomingStatus.o2);
        incomingStatus.lel = pickNewestReading(currentStatus.lel, incomingStatus.lel);
        incomingStatus.h2s = pickNewestReading(currentStatus.h2s, incomingStatus.h2s);

        const currentLastActive = getTimestampMillis(currentStatus.lastActive);
        const incomingLastActive = getTimestampMillis(incomingStatus.lastActive);
        if (
          currentLastActive !== undefined &&
          incomingLastActive !== undefined &&
          incomingLastActive < currentLastActive
        ) {
          incomingStatus.lastActive = currentStatus.lastActive;
        }

        updated.status = incomingStatus;
        return updated;
      });
    },
    []
  );

  useDeviceStatusStreams({
    deviceIds: liveStatusDeviceIds,
    onStatusUpdate: handleLiveDeviceStatus,
    token,
    keys: liveContextKeys,
    types: liveContextTypes,
    as: liveAs,
    enabled: Boolean(deviceID && token),
  });

  const loadDevice = () => {
    if (loading) return
    if (state?.devicePageData) {
      setDevice(Device.create(state.devicePageData.device))
      setComponents(state.devicePageData.components.map((comp: pond.Component) => Component.create(comp)))
      setInteractions(state.devicePageData.interactions.map((inter: pond.Interaction) => Interaction.create(inter)))
      setPreferences(state.devicePageData.preferences)
      setPermissions(state.devicePageData.permissions)
      return
    }
    setLoading(true)
    deviceAPI.getPageData(deviceID, getContextKeys(), getContextTypes(), as).then(resp => {
      // setDevicePageData(resp.data)
      let device = Device.any(resp.data.device)
      // console.log(resp.data)
      setScanInProgress(resp.data.pendingRequests)
      setDevice(device)
      let newPermissions: pond.Permission[] = []
      resp.data.permissions.forEach(perm => {
        if (typeof(perm) === "string" ) {
          const permNumber = pond.Permission[perm as keyof typeof pond.Permission]
          newPermissions.push(permNumber)
        } else {
          newPermissions.push(perm)
        }
      })
      setPermissions(newPermissions)
      let u = User.any(resp.data.user);
      setPreferences(u.preferences)
      // setTags(resp.data.tags)
      resp.data.device?.status?.tagNames
      let newComps: Component[] = []
      resp.data.components.forEach(comp => {
        newComps.push(Component.create(comp))
      })
      let available = FindAvailablePositions(
        newComps,
        device.settings.product
      );
      setAvailablePositions(available.GetAvailability());
      let rawComponents: Array<any> = or(resp.data.components, []);
      let rComponents: Map<string, Component> = new Map();
      let diagComponents: Component[] = [];
      rawComponents.forEach((rawComponent: any) => {
        let component = Component.any(rawComponent);
        rComponents.set(component.key(), component);
        //check if is a diagnostic component
        switch (component.type()) {
          case quack.ComponentType.COMPONENT_TYPE_GRAIN_CABLE:
            if (component.subType() === quack.GrainCableSubtype.GRAIN_CABLE_SUBTYPE_OPI_DIAG) {
              diagComponents.push(component);
            }
        }
      });
      setDiagnosticComponents(diagComponents)
      setComponents(rComponents)
      let interactions: Interaction[] = [];
      resp.data.interactions.forEach((interaction: pond.Interaction) => {
        if (
          interaction.settings &&
          interaction.settings.nodeOne > interaction.settings.nodeTwo &&
          interaction.settings.nodeTwo !== 0
        ) {
          //flip operator to display what is actually happening
          interaction.settings.conditions.forEach(condition => {
            if (condition.comparison === quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN) {
              condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_LESS_THAN;
            } else {
              condition.comparison = quack.RelationalOperator.RELATIONAL_OPERATOR_GREATER_THAN;
            }
            condition.value = condition.value * -1;
          });
        }
        interactions.push(Interaction.any(interaction));
      });
      setInteractions(interactions);
    }).catch(err => {
      setDevice(Device.create());
      // setComponents(new Map());
      setInteractions([]);
      setAvailablePositions(new Map());
      setPermissions([]);
      setPreferences(pond.UserPreferences.create());
      // setInvalidDevice(true);
      // error(err);
      if (err?.response?.data?.error.includes("not found")) {
        let name = as === team.key() ? team.name() : user.name();
        let warningString = name + " not permitted to view Device " + deviceID;
        let length = getContextTypes().length;
        if (length > 0) {
          warningString = warningString + " through " + getContextTypes()[length - 1];
        }
        snackbar.warning("page data warning: "+warningString);
      } else {
        snackbar.error("page data error: "+err);
      }
    })
    .finally(() => setLoading(false));
  }

  // --- Real-time component updates ---
  // Streams all component changes for this device.
  // When the backend receives a new reading and updates a component,
  // this fires and updates the component in state without a page refresh.
  useWebSocket<{ key: string; component: Component } | null>({
    path: `/v1/live/devices/${deviceID}/components`,
    parse: (e) => {
      try {
        const raw = JSON.parse(e.data);
        const comp = Component.any(raw);
        if (!comp?.settings) {
          console.warn("[ws] received component without settings:", raw);
          return null;
        }
        return { key: comp.key(), component: comp };
      } catch (err) {
        console.warn("[ws] failed to parse component:", err);
        return null;
      }
    },
    onMessage: (data) => {
      if (!data) return;
      const { key, component } = data;
      setComponents((prev) => {
        const updated = new Map(prev);
        updated.set(key, component);
        return updated;
      });
    },
    keys: liveContextKeys,
    types: liveContextTypes,
    as: liveAs,
    token,
    enabled: !!deviceID,
  });

  const loadPortScan = () => {
    deviceAPI.listFoundComponents(parseInt(deviceID))
      .then(resp => {
        if (resp.data.foundComponents?.i2c || resp.data.foundComponents?.oneWire){
          setScannedAddresses(pond.ListFoundComponentsResponse.fromObject(resp.data).foundComponents ?? undefined)
        }
      })
      .catch(err => {
        console.log("load port scan: "+err)
      })
  }

  useEffect(() => {
    loadDevice()
    loadPortScan()
  }, [deviceID, as])

  const toggleNotificationPreference = () => {
    let updatedPreferences = cloneDeep(preferences);
    updatedPreferences.notify = !preferences.notify;
    deviceAPI
      .updatePreferences(deviceID, updatedPreferences, getContextKeys(), getContextTypes(), as)
      .then(() => setPreferences(updatedPreferences))
      .catch(() => {
        snackbar.error(
          "Error occured while " +
            (preferences.notify ? "enabling" : "disabling") +
            " notifications"
        );
      });
  };

  if (loading) return (
    <LoadingScreen message="Loading device"/>
  )

  const getUsage = () => {
    let usage = undefined;
    if (cellularStatus && cellularStatus !== "") {
      usage = { status: cellularStatus, bytes: cellularUsage };
    }
    return usage;
  };

  const displayedComponentsBlacklist = [
    quack.ComponentType.COMPONENT_TYPE_POWER,
    quack.ComponentType.COMPONENT_TYPE_MODEM
  ];

  const getOrderedComponents = () => {
    let compsToOrder: Component[] = [];
    Array.from(components.values()).forEach(comp => {
      if (!displayedComponentsBlacklist.includes(comp.settings.type)) {
        compsToOrder.push(comp);
      }
    });
    if (displayOrder.length === compsToOrder.length) {
      return compsToOrder.sort((a, b: Component) => sortComponents(a, b, displayOrder));
    } else {
      return compsToOrder;
    }
  };

  const handleComponentChanged = (component: Component) => {
    let updatedComponents = cloneDeep(components);
    let updatedComponent = cloneDeep(component);
    if (updatedComponents.has(updatedComponent.key())) {
      updatedComponents.set(updatedComponent.key(), updatedComponent);
      setComponents(updatedComponents);
    }
  };

  const componentCards = () => {
    let orderedComponents = getOrderedComponents();
    let sensorComponents: JSX.Element[] = [];
    let controllerComponents: JSX.Element[] = [];
    //let componentCards = [];
    //let hasGPS = false;
    for (let i = 0; i < orderedComponents.length; i++) {
      let c = orderedComponents[i];
      if (displayedComponentsBlacklist.includes(c.settings.type)) {
        continue;
      }
      let id: quack.IComponentID = quack.ComponentID.fromObject({
        type: c.settings.type,
        addressType: c.settings.addressType,
        address: c.settings.address,
        expansionLine: c.settings.expansionLine,
        muxLine: c.settings.muxLine
      });
      let filteredInteractions = interactions.filter(interaction => {
        let isSource = false;
        let isSink = false;
        if (interaction.settings) {
          isSource = sameComponentID(interaction.settings.source, id);
          isSink = sameComponentID(interaction.settings.sink, id);
        }
        return isSource || isSink;
      });
      // if (id.type === quack.ComponentType.COMPONENT_TYPE_GPS) {
      //   hasGPS = true;
      // }
      if (isController(c.settings.type)) {
        controllerComponents.push(
          <ComponentCard
            device={device}
            availablePositions={availablePositions}
            availableOffsets={availableOffsets}
            component={c}
            components={orderedComponents}
            interactions={filteredInteractions}
            permissions={permissions}
            deviceComponentPreferences={prefsMap.get(c.key())}
            key={i}
            refreshCallback={(updatedComponent?: Component) =>
              updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
            }
          />
        );
      } else {
        sensorComponents.push(
          <ComponentCard
            device={device}
            availablePositions={availablePositions}
            availableOffsets={availableOffsets}
            component={c}
            components={orderedComponents}
            interactions={filteredInteractions}
            permissions={permissions}
            deviceComponentPreferences={prefsMap.get(c.key())}
            key={i}
            refreshCallback={(updatedComponent?: Component) =>
              updatedComponent ? handleComponentChanged(updatedComponent) : loadDevice()
            }
          />
        );
      }
    }
    return (
      <List>
        <li>
          <Grid container direction="row" justifyContent="space-between">
            <Grid >
              <Typography display="block" variant="h6">
                Sensors
              </Typography>
            </Grid>
            <Grid >
              {/* <FormControlLabel
                label="Show Errors"
                control={
                  <Checkbox
                    checked={showErrors}
                    onChange={() => {
                      //setShowErrors(!showErrors);
                      dispatch({ key: "showErrors", value: !showErrors });
                    }}
                  />
                }
              /> */}
            </Grid>
          </Grid>
        </li>
        <Divider component="li" />
        <ListItem>
          <Grid width={"100%"} container justifyContent="flex-start" direction="row" spacing={2}>
            {sensorComponents.map((card, index) => (
              <Grid key={"sensor-components-"+index} size={{ 
                xs: 12, 
                sm: isMobile ? 12 : 6, 
                md: isMobile ? 12 : 6,
                lg: isMobile ? 12 : 4,
                xl: isMobile ? 12 : 3
              }}>
                {card}
              </Grid>
            ))}
          </Grid>
        </ListItem>
        <li>
          <Typography display="block" variant="h6">
            Controls
          </Typography>
        </li>
        <Divider component="li" />
        <ListItem>
          <Grid width={"100%"} container justifyContent="flex-start" direction="row" spacing={2}>
            {controllerComponents.map((card, index) => (
              <Grid key={"sensor-components-"+index} size={{ 
                xs: 12, 
                sm: isMobile ? 12 : 6, 
                md: isMobile ? 12 : 6,
                lg: isMobile ? 12 : 4,
                xl: isMobile ? 12 : 3
              }}>
                {card}
              </Grid>
            ))}
          </Grid>
        </ListItem>
        {device.settings.mutations.length > 0 && (
          <>
            <li>
              <Typography display="block" variant="h6">
                Calculated Measurements
              </Typography>
            </li>
            <Divider component="li" />
            <ListItem>
              <Grid container direction="row" justifyContent="flex-start" spacing={2}>
                {/* {device.settings.mutations.map((mut, i) => (
                  <DeviceMutationCard
                    key={"mutation" + i}
                    deviceId={device.id()}
                    mutation={mut}
                    showMobile={isMobile}
                  />
                ))} */}
              </Grid>
            </ListItem>
          </>
        )}
        {device.settings.product !== pond.DeviceProduct.DEVICE_PRODUCT_NONE &&
          permissions.includes(pond.Permission.PERMISSION_WRITE) && (
            <>
              <li>
                <Typography display="block" variant="h6">
                  Setup
                </Typography>
              </li>
              <Divider component="li" />
              <ListItem>
                <Grid container direction="row" spacing={2} width={"100%"}>
                  {isMobile ? (
                    <>
                      <Grid size={{ xs: 12 }}>
                        <DeviceWizard
                          device={device}
                          scanInProgress={scanInProgress}
                          //permissions={permissions}
                          components={Array.from(components.values())}
                          refreshCallback={loadDevice}
                        />
                      </Grid>
                      {/* add grid card here for I2C detected components */}
                      {(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
                        <Grid size={{ xs: 12 }}>
                          <DeviceScannedComponents 
                            scannedComponents={scannedAddresses} 
                            device={device} 
                            availablePositions={availablePositions}
                            availableOffsets={availableOffsets}
                            refreshCallback={loadDevice}/>
                        </Grid>
                      }
                      {diagnosticComponents.map(comp => (
                        <Grid size={{ xs: 12 }} key={comp.key()}>
                          <ComponentDiagnostics
                            component={comp}
                            device={device}
                            refreshCallback={loadDevice}
                          />
                        </Grid>
                      ))}
                    </>
                  ) : (
                    <>
                      <Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }} >
                        <DeviceWizard
                          device={device}
                          //permissions={permissions}
                          components={Array.from(components.values())}
                          refreshCallback={loadDevice}
                        />
                      </Grid>
                      {(scannedAddresses?.i2c || scannedAddresses?.oneWire) &&
                        <Grid size={{ xs: 6, sm: 6, lg: 8, xl: 8 }}>
                          <DeviceScannedComponents 
                            scannedComponents={scannedAddresses} 
                            device={device} 
                            availablePositions={availablePositions}
                            availableOffsets={availableOffsets}
                            refreshCallback={loadDevice}/>
                        </Grid>
                      }
                      {diagnosticComponents.length > 0 &&
                      <Grid size={{ xs: 6, sm: 6, lg: 4, xl: 4 }}>
                        <Grid container direction="row" spacing={2} width={"100%"}>
                          {diagnosticComponents.map(comp => (
                            <Grid  size={12} key={comp.key()}>
                              <ComponentDiagnostics
                                component={comp}
                                device={device}
                                refreshCallback={loadDevice}
                              />
                            </Grid>
                          ))}
                        </Grid>
                      </Grid>
                      }
                    </>
                  )}
                </Grid>
              </ListItem>
            </>
          )}
      </List>
    );
  }

  return (
    <PageContainer spacing={isMobile ? 0 : 2}>
      <Grid container justifyContent={"space-between"}>
        <Grid>
          <SmartBreadcrumb deviceName={device.name()} />
        </Grid>
        <Grid>
          <DeviceActions
            device={device}
            isPaused={false}
            isLoading={loading}
            permissions={permissions}
            preferences={preferences}
            toggleNotificationPreference={toggleNotificationPreference}
            refreshCallback={loadDevice}
            availablePositions={availablePositions}
            availableOffsets={availableOffsets} 
            components={[...components.values()]}
            interactions={interactions}    
          />
        </Grid>
      </Grid>
      <DeviceOverview
        device={device}
        components={[...components.values()]}
        usage={getUsage()}
        loading={loading}
        groupID={parseInt(groupID)}
      />
      {componentCards()}
    </PageContainer>
  );
}