/*
 * Copyright 2025 The Kubernetes Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Link, SectionBox, SimpleTable } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import { Autocomplete, Box, Button, Chip, Stack, TextField, Typography } from '@mui/material';
import React from 'react';
import type { KRevisionResource, KService, Traffic } from '../../../../../resources/knative';
import { getAge } from '../../../../../utils/time';
import { getSafeUrl } from '../../../../../utils/url';
import { useNotify } from '../../../../common/notifications/useNotify';
import { ReadyStatusLabel } from '../../../../common/ReadyStatusLabel';
import { useKServiceEditMode } from '../../hooks/useKServiceEditMode';
import { useKServicePermissions } from '../../permissions/KServicePermissionsProvider';

type Props = {
  cluster: string;
  kservice: KService;
  revisions: KRevisionResource[];
};

/**
 * Represents the base data model for a row within the traffic splitting configuration table.
 *
 * @property {string} id - The unique identifier corresponding to the Knative revision name.
 * @property {boolean} isLatestRow - Flags whether this row represents the singular cluster-wide "latest ready" revision.
 * @property {string} nameLabel - The display-friendly name shown to the user in the data grid.
 * @property {boolean} showLatestBadge - Determines whether a specialized 'Latest' informational chip should render next to the name.
 * @property {string} [badgeLabel] - The textual content rendered inside the 'Latest' chip, typically matching the latest revision name.
 * @property {boolean} showUnavailableBadge - Determines whether a specialized 'Unavailable' warning chip should render when the revision lacks readiness.
 * @property {boolean} hasStatus - Indicates if the underlying revision object possesses valid condition array data.
 * @property {Object} [readyCond] - A subset of the status object extracting critical fields related to immediate component readiness.
 * @property {string} [creationTimestamp] - The ISO string corresponding to when the cluster initially recorded the generation of this revision.
 */
export interface TrafficTableRow {
  id: string;
  isLatestRow: boolean;
  nameLabel: string;
  showLatestBadge: boolean;
  badgeLabel?: string;
  showUnavailableBadge: boolean;
  hasStatus: boolean;
  readyCond?: { status?: string; reason?: string; message?: string };
  creationTimestamp?: string;
}

/**
 * Extends the base traffic entry with specific UI data points needed during read/edit operations
 * for distributing requests among Revisions.
 *
 * @property {number} percent - The integer value representing the portion of overall routing this entry is assigned.
 * @property {string[]} tags - Identifiers applied to this route for discrete URL resolution and reference targeting.
 * @example
 * const row: TrafficTableRowData = { ...baseData, percent: 80, tags: ['canary'] };
 */
export interface TrafficTableRowData extends TrafficTableRow {
  percent: number;
  tags: string[];
}

/**
 * Governs the properties for the uncoupled presentation component that renders the traffic grid.
 *
 * @property {TrafficTableRowData[]} tableData - Array detailing the configuration and distribution states across all targetable revisions.
 * @property {number} totalTraffic - Aggregate calculation of all row portions. Utilized to ensure mathematical validation (summing to 100).
 * @property {boolean} isReadOnly - Determines if the interface restricts input elements, enforcing a view-only mode based on permissions or state.
 */
export interface PureTrafficSplittingSectionProps {
  tableData: TrafficTableRowData[];
  totalTraffic: number;
  isReadOnly: boolean;
}

export default function TrafficSplittingSection({ cluster, kservice, revisions }: Props) {
  const [savingTraffic, setSavingTraffic] = React.useState(false);
  const { canPatchKService, isLoading } = useKServicePermissions();
  const { isEditMode } = useKServiceEditMode();
  const isReadOnly = !isEditMode || canPatchKService !== true || isLoading;
  const [revPercents, setRevPercents] = React.useState<Record<string, number>>({});
  const [revTags, setRevTags] = React.useState<Record<string, string[]>>({});
  const [latestPercent, setLatestPercent] = React.useState<number>(0);
  const [latestTags, setLatestTags] = React.useState<string[]>([]);
  const { notifySuccess, notifyError } = useNotify();
  const revTagInputRefs = React.useRef<Record<string, HTMLInputElement | null>>({});
  const latestTagInputRef = React.useRef<HTMLInputElement | null>(null);
  const [pendingTagInputs, setPendingTagInputs] = React.useState<Record<string, string>>({});

  const initializeFromKService = React.useCallback(() => {
    if (!kservice || !revisions) return;
    const trafficEntries = kservice.spec?.traffic || kservice.status?.traffic || [];
    const byRev = new Map<string, { percent: number; tags: Set<string> }>();
    let latestPercentTotal = 0;
    const latestTagsSet = new Set<string>();
    (trafficEntries || []).forEach(traffic => {
      if (traffic.latestRevision) {
        latestPercentTotal += Number(traffic.percent || 0);
        if (traffic.tag) {
          latestTagsSet.add(traffic.tag);
        }
        return;
      }
      const target = traffic.revisionName;
      if (!target) return;
      const info = byRev.get(target) || { percent: 0, tags: new Set<string>() };
      info.percent += Number(traffic.percent || 0);
      if (traffic.tag) {
        info.tags.add(traffic.tag);
      }
      byRev.set(target, info);
    });
    const nextPercents: Record<string, number> = {};
    const nextTags: Record<string, string[]> = {};
    for (const r of revisions) {
      const revName = r.metadata.name;
      const info = byRev.get(revName);
      nextPercents[revName] = info?.percent ?? 0;
      nextTags[revName] = info ? Array.from(info.tags) : [];
    }
    setRevPercents(nextPercents);
    setRevTags(nextTags);
    setLatestPercent(latestPercentTotal);
    setLatestTags(Array.from(latestTagsSet));
  }, [kservice, revisions]);

  React.useEffect(() => {
    initializeFromKService();
  }, [initializeFromKService]);

  const totalTraffic = React.useMemo(() => {
    const revisionTotal = Object.values(revPercents).reduce((acc, v) => acc + (Number(v) || 0), 0);
    return revisionTotal + (Number(latestPercent) || 0);
  }, [revPercents, latestPercent]);

  const allTags = React.useMemo(() => {
    const tags: string[] = [];
    Object.values(revTags).forEach(list => {
      list.forEach(tag => {
        const trimmed = tag.trim();
        if (trimmed) tags.push(trimmed);
      });
    });
    latestTags.forEach(tag => {
      const trimmed = tag.trim();
      if (trimmed) tags.push(trimmed);
    });
    return tags;
  }, [revTags, latestTags]);

  const hasDuplicateTags = React.useMemo(() => {
    const seen = new Set<string>();
    for (const tag of allTags) {
      if (seen.has(tag)) return true;
      seen.add(tag);
    }
    return false;
  }, [allTags]);

  const latestReadyRevisionName = kservice?.status?.latestReadyRevisionName;
  const latestReadyRevision = React.useMemo(() => {
    if (!latestReadyRevisionName || !revisions) return undefined;
    return revisions.find(r => r.metadata.name === latestReadyRevisionName);
  }, [revisions, latestReadyRevisionName]);

  const trafficValidationError = React.useMemo(() => {
    if (!revisions?.length) return 'No revisions available';
    for (const key of Object.keys(revPercents)) {
      const val = Number(revPercents[key]);
      if (Number.isNaN(val) || val < 0 || val > 100) {
        return 'Traffic percentages must be between 0 and 100';
      }
    }
    const latestVal = Number(latestPercent);
    if (Number.isNaN(latestVal) || latestVal < 0 || latestVal > 100) {
      return 'Latest revision percent must be between 0 and 100';
    }
    if (totalTraffic !== 100) {
      return 'Total traffic must equal 100%';
    }
    if (hasDuplicateTags) {
      return 'Tags must be unique';
    }
    return null;
  }, [revPercents, totalTraffic, latestPercent, hasDuplicateTags, revisions]);

  const pendingTagError = React.useMemo(
    () =>
      Object.values(pendingTagInputs).some(v => Boolean(v?.trim()))
        ? 'There are unconfirmed tags. Please press Enter to confirm.'
        : null,
    [pendingTagInputs]
  );
  const combinedValidationError = trafficValidationError || pendingTagError;
  const isTrafficValid = !combinedValidationError;

  function resetSection() {
    initializeFromKService();
    setPendingTagInputs({});

    // Clear any in-progress tag inputs in the underlying text fields.
    Object.keys(revTagInputRefs.current).forEach(name => {
      const ref = revTagInputRefs.current[name];
      if (ref) {
        ref.value = '';
      }
    });
    if (latestTagInputRef.current) {
      latestTagInputRef.current.value = '';
    }
  }

  async function onSaveTraffic() {
    if (!kservice || !revisions) return;
    if (!cluster) {
      notifyError('No cluster available');
      return;
    }

    setSavingTraffic(true);
    try {
      // 1) Build a merged result that includes in-progress (unconfirmed) tag input
      const mergedRevTags: Record<string, string[]> = {};
      const revisionNames = Array.from(
        new Set([
          ...Object.keys(revPercents),
          ...Object.keys(revTags),
          ...Object.keys(revTagInputRefs.current),
        ])
      );
      revisionNames.forEach(revisionName => {
        const base = revTags[revisionName] || [];
        const pending = (revTagInputRefs.current[revisionName]?.value || '').trim();
        const unique = Array.from(
          new Set([...base, ...(pending ? [pending] : [])].map(v => v.trim()).filter(Boolean))
        );
        mergedRevTags[revisionName] = unique;
      });
      const pendingLatest = (latestTagInputRef.current?.value || '').trim();
      const mergedLatestTags = Array.from(
        new Set(
          [...latestTags, ...(pendingLatest ? [pendingLatest] : [])]
            .map(v => v.trim())
            .filter(Boolean)
        )
      );

      // 2) Validation (also check duplicates after merging)
      const revisionTotal = Object.values(revPercents).reduce(
        (acc, v) => acc + (Number(v) || 0),
        0
      );
      const total = revisionTotal + (Number(latestPercent) || 0);
      let validationError: string | null = null;

      for (const key of Object.keys(revPercents)) {
        const val = Number(revPercents[key]);
        if (Number.isNaN(val) || val < 0 || val > 100) {
          validationError = 'Traffic percentages must be between 0 and 100';
          break;
        }
      }
      if (!validationError) {
        const lp = Number(latestPercent);
        if (Number.isNaN(lp) || lp < 0 || lp > 100) {
          validationError = 'Latest revision percent must be between 0 and 100';
        }
      }
      if (!validationError && total !== 100) {
        validationError = 'Total traffic must equal 100%';
      }
      if (!validationError) {
        const seen = new Set<string>();
        const all: string[] = [];
        Object.values(mergedRevTags).forEach(list => list.forEach(t => all.push(t)));
        mergedLatestTags.forEach(t => all.push(t));
        for (const t of all) {
          if (seen.has(t)) {
            validationError = 'Tags must be unique';
            break;
          }
          seen.add(t);
        }
      }
      if (validationError) {
        // Also reflect the in-progress input values into UI state
        setRevTags(prev => {
          const next: Record<string, string[]> = { ...prev };
          revisionNames.forEach(name => {
            next[name] = mergedRevTags[name] || [];
          });
          return next;
        });
        setLatestTags(mergedLatestTags);
        notifyError(validationError);
        return;
      }

      // 3) Build Traffic payload for submission (using merged tags)
      const traffic: Traffic[] = [];
      revisionNames.forEach(revisionName => {
        const numericPercent = Number(revPercents[revisionName] ?? 0) || 0;
        const tags = mergedRevTags[revisionName] || [];
        if (numericPercent > 0) {
          traffic.push({
            revisionName,
            percent: numericPercent,
          });
        }
        tags.forEach(tag => {
          traffic.push({
            revisionName,
            percent: 0,
            tag,
          });
        });
      });
      const latestPercentValue = Number(latestPercent) || 0;
      if (latestPercentValue > 0) {
        traffic.push({
          latestRevision: true,
          percent: latestPercentValue,
        });
      }
      mergedLatestTags.forEach(tag => {
        traffic.push({
          latestRevision: true,
          percent: 0,
          tag,
        });
      });

      await kservice.patch({
        spec: {
          traffic: traffic,
        },
      });

      // 4) On success, sync UI state to merged values and clear inputs
      setRevTags(prev => {
        const next: Record<string, string[]> = { ...prev };
        revisionNames.forEach(name => {
          next[name] = mergedRevTags[name] || [];
        });
        return next;
      });
      setLatestTags(mergedLatestTags);
      revisionNames.forEach(name => {
        const ref = revTagInputRefs.current[name];
        if (ref) ref.value = '';
      });
      if (latestTagInputRef.current) latestTagInputRef.current.value = '';

      notifySuccess('Traffic updated');
    } catch (err: unknown) {
      const error = err as { message?: string } | undefined;
      const detail = error?.message?.trim();
      notifyError(detail ? `Failed to update traffic: ${detail}` : 'Failed to update traffic');
    } finally {
      setSavingTraffic(false);
    }
  }

  React.useEffect(() => {
    if (!isEditMode) {
      resetSection();
    }
  }, [isEditMode, resetSection]);

  const getTagUrl = (tagName: string) => {
    const trafficEntry = kservice.status?.traffic?.find(t => t.tag === tagName);
    return trafficEntry?.url;
  };

  const columns = [
    {
      label: 'Name',
      getter: (original: TrafficTableRow) => {
        const namespace = kservice.metadata.namespace!;
        const revisionName = original.isLatestRow ? original.badgeLabel : original.id;
        return (
          <Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
            {revisionName ? (
              <Link
                routeName="revisionDetails"
                params={{ namespace: namespace, name: revisionName }}
                activeCluster={cluster}
              >
                {original.nameLabel}
              </Link>
            ) : (
              <Typography variant="body2">{original.nameLabel}</Typography>
            )}

            {original.showLatestBadge && original.badgeLabel && (
              <Chip label={original.badgeLabel} color="info" size="small" variant="outlined" />
            )}

            {original.showUnavailableBadge && (
              <Chip label="Unavailable" color="warning" size="small" variant="outlined" />
            )}
          </Stack>
        );
      },
      sort: (a: TrafficTableRow, b: TrafficTableRow) => {
        return a.nameLabel.localeCompare(b.nameLabel);
      },
    },
    {
      label: 'Ready',
      getter: (original: TrafficTableRow) => {
        let status: 'True' | 'False' | 'Unknown' = 'Unknown';
        if (original.hasStatus && original.readyCond?.status) {
          const condStatus = original.readyCond.status;
          if (condStatus === 'True' || condStatus === 'False' || condStatus === 'Unknown') {
            status = condStatus;
          } else {
            status = 'Unknown';
          }
        }
        return (
          <ReadyStatusLabel
            status={status}
            reason={original.readyCond?.reason}
            message={original.readyCond?.message}
          />
        );
      },
    },
    {
      label: 'Age',
      getter: (original: TrafficTableRow) =>
        original.creationTimestamp ? getAge(original.creationTimestamp) : '-',
    },
    {
      label: 'Traffic',
      getter: (original: TrafficTableRow) => {
        const val = original.isLatestRow ? latestPercent : revPercents[original.id] ?? 0;

        if (isReadOnly) {
          return <Typography variant="body2">{val}%</Typography>;
        }

        return (
          <TextField
            type="number"
            size="small"
            inputProps={{ min: 0, max: 100, step: 1, inputMode: 'numeric' }}
            onFocus={e => {
              try {
                (e.target as HTMLInputElement).select();
              } catch {
                // noop
              }
            }}
            value={val}
            onChange={e => {
              const numeric = Number(e.target.value);
              if (original.isLatestRow) {
                setLatestPercent(Number.isNaN(numeric) ? 0 : numeric);
              } else {
                setRevPercents(prev => ({
                  ...prev,
                  [original.id]: numeric,
                }));
              }
            }}
            sx={{ width: 100 }}
          />
        );
      },
      sort: (a: TrafficTableRow, b: TrafficTableRow) => {
        const valA = a.isLatestRow ? latestPercent : revPercents[a.id] ?? 0;
        const valB = b.isLatestRow ? latestPercent : revPercents[b.id] ?? 0;
        return valA - valB;
      },
    },
    {
      label: 'Tags',
      getter: (original: TrafficTableRow) => {
        const tags = original.isLatestRow ? latestTags : revTags[original.id] || [];

        if (isReadOnly) {
          if (tags.length === 0) {
            return (
              <Typography variant="body2" color="text.secondary">
                -
              </Typography>
            );
          }
          return (
            <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap' }}>
              {tags.map((tag, i) => {
                const url = getSafeUrl(getTagUrl(tag));
                return url ? (
                  <Chip
                    key={i}
                    label={tag}
                    size="small"
                    component="a"
                    href={url}
                    target="_blank"
                    rel="noopener noreferrer"
                    clickable
                    color="primary"
                  />
                ) : (
                  <Chip key={i} label={tag} size="small" />
                );
              })}
            </Stack>
          );
        }

        return (
          <Autocomplete<string, true, false, true>
            multiple
            freeSolo
            size="small"
            value={tags}
            options={[]}
            filterSelectedOptions
            onChange={(_, newValue) => {
              const unique = Array.from(new Set(newValue.map(v => v.trim()).filter(Boolean)));
              if (original.isLatestRow) {
                setLatestTags(unique);
                setPendingTagInputs(prev => ({ ...prev, latest: '' }));
              } else {
                setRevTags(prev => ({ ...prev, [original.id]: unique }));
                setPendingTagInputs(prev => ({ ...prev, [original.id]: '' }));
              }
            }}
            renderTags={(value, getTagProps) =>
              value.map((option, index) => (
                <Chip
                  {...getTagProps({ index })}
                  key={`${option}-${index}`}
                  label={option}
                  size="small"
                />
              ))
            }
            renderInput={params => {
              const pendingKey = original.isLatestRow ? 'latest' : original.id;
              const hasError = Boolean(pendingTagInputs[pendingKey]?.trim());

              return (
                <TextField
                  {...params}
                  placeholder="Add tag"
                  inputRef={el => {
                    if (original.isLatestRow) {
                      latestTagInputRef.current = el;
                    } else {
                      revTagInputRefs.current[original.id] = el;
                    }
                  }}
                  error={hasError}
                  helperText={hasError ? 'Press Enter to confirm the tag' : undefined}
                  onChange={e => {
                    params.inputProps.onChange?.(e as React.ChangeEvent<HTMLInputElement>);
                    setPendingTagInputs(prev => ({
                      ...prev,
                      [pendingKey]: e.target.value,
                    }));
                  }}
                />
              );
            }}
            sx={{ minWidth: 220 }}
          />
        );
      },
      sort: (a: TrafficTableRow, b: TrafficTableRow) => {
        const tagsA = a.isLatestRow ? latestTags : revTags[a.id] || [];
        const tagsB = b.isLatestRow ? latestTags : revTags[b.id] || [];
        return tagsA.join(',').localeCompare(tagsB.join(','));
      },
    },
  ];

  const sortedRevisions = [...revisions].sort((a, b) => {
    const at = new Date(a.metadata.creationTimestamp || 0).getTime();
    const bt = new Date(b.metadata.creationTimestamp || 0).getTime();
    return bt - at;
  });

  const latestReadyCondition = latestReadyRevision?.status?.conditions?.find(
    c => c.type === 'Ready'
  );

  const tableData: TrafficTableRow[] = [
    {
      id: 'latest',
      isLatestRow: true,
      nameLabel: 'Latest Ready Revision',
      showLatestBadge: Boolean(latestReadyRevisionName),
      badgeLabel: latestReadyRevisionName,
      showUnavailableBadge: !latestReadyRevisionName,
      hasStatus: Boolean(latestReadyRevision),
      readyCond: latestReadyCondition,
      creationTimestamp: latestReadyRevision?.metadata.creationTimestamp,
    },
    ...sortedRevisions.map(r => ({
      id: r.metadata.name,
      isLatestRow: false,
      nameLabel: r.metadata.name,
      showLatestBadge: latestReadyRevisionName === r.metadata.name,
      badgeLabel: 'Latest Ready',
      showUnavailableBadge: false,
      hasStatus: true,
      readyCond: r.status?.conditions?.find(c => c.type === 'Ready'),
      creationTimestamp: r.metadata.creationTimestamp,
    })),
  ];

  return (
    <SectionBox title="Traffic Splitting">
      <Stack spacing={2}>
        <SimpleTable columns={columns} data={tableData} />
        <Box
          mt={2}
          display="flex"
          justifyContent="space-between"
          alignItems="center"
          flexWrap="wrap"
          gap={1}
        >
          <Box display="flex" flexDirection="column">
            <Typography variant="body2" color={isTrafficValid ? 'text.secondary' : 'error'}>
              Total: {totalTraffic}% (must equal 100%)
            </Typography>
            {!isTrafficValid && combinedValidationError && (
              <Typography variant="caption" color="error">
                {combinedValidationError}
              </Typography>
            )}
          </Box>
          <Box display="flex" gap={1}>
            {!isReadOnly && (
              <Button variant="text" onClick={resetSection} aria-label="Reset traffic">
                Reset
              </Button>
            )}
            {!isReadOnly && (
              <Button
                variant="contained"
                onClick={onSaveTraffic}
                disabled={!isTrafficValid || savingTraffic}
                aria-label="Save traffic"
              >
                {savingTraffic ? 'Saving…' : 'Save'}
              </Button>
            )}
          </Box>
        </Box>
      </Stack>
    </SectionBox>
  );
}
