import hljs from 'highlight.js/lib/core';
import { StrictMode, useEffect, useMemo, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import 'highlight.js/styles/hybrid.css';
import styles from './api.module.css';

hljs.registerLanguage('json', require('highlight.js/lib/languages/json'));
hljs.registerLanguage(
  'plaintext',
  require('highlight.js/lib/languages/plaintext'),
);

const CodeBlock = ({
  value,
  language,
  originalValue,
}: { value: string; language?: string | null; originalValue?: string }) => {
  const codeRef = useRef<HTMLPreElement>(null);
  const langClass = language ? `language-${language}` : undefined;

  // biome-ignore lint/correctness/useExhaustiveDependencies:
  useEffect(() => {
    if (codeRef.current && !codeRef.current.dataset.highlighted) {
      hljs.highlightElement(codeRef.current);
    }
  }, [value]);

  const copyToClipboard = () => {
    navigator.clipboard.writeText(originalValue || value).catch((e) => {
      console.error('Failed to copy text', e);
    });
  };

  return (
    <pre className={styles.codeBlock}>
      <button
        className={`secondary ${styles.copyButton} icon-copy`}
        onClick={copyToClipboard}
      />
      <code ref={codeRef} className={langClass}>
        {value}
      </code>
    </pre>
  );
};

type ProjectsResponse = {
  projects: ProjectResponse[];
};

type ProjectResponse = {
  id: string;
  owner: string;
  repo: string;
  name: string | null;
  short_name: string | null;
  platform: string | null;
  default_version: string;
  report_versions: string[];
  report_categories: CategoryResponse[];
};

type CategoryResponse = {
  id: string;
  name: string;
};

const fetchProjects = async () => {
  const url = new URL('/projects', window.location.origin);
  url.searchParams.append('sort', 'name');
  const response = await fetch(url, {
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) {
    throw new Error('Failed to fetch projects');
  }
  return (await response.json()) as ProjectsResponse;
};

const fetchProjectById = async (id: string, version: string | null) => {
  const url = new URL(`/projects/${id}`, window.location.origin);
  if (version) {
    url.searchParams.append('version', version);
  }
  const response = await fetch(url, {
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) {
    throw new Error('Failed to fetch projects');
  }
  return (await response.json()) as ProjectResponse;
};

const fetchApiUrl = async (url: string) => {
  const response = await fetch(url, {
    headers: { Accept: 'application/json' },
  });
  if (!response.ok) {
    throw new Error('Failed to fetch projects');
  }
  return await response.text();
};

const ProjectForm = () => {
  const [projects, setProjects] = useState<ProjectResponse[]>([]);
  const [loadingProjects, setLoadingProjects] = useState(true);
  const [loadingProject, setLoadingProject] = useState(true);

  type Selected = {
    project: string | null;
    version: string | null;
    category: string | null;
    subcategory: string | null;
  };
  const [selected, setSelected] = useState<Selected>({
    project: null,
    version: null,
    category: null,
    subcategory: null,
  });
  const [mode, setMode] = useState('overview');
  const [format, setFormat] = useState('json');
  const [formatOptions, setFormatOptions] = useState<any>({});
  const [useProjectId, setUseProjectId] = useState(false);

  const [currentProject, setCurrentProject] = useState<ProjectResponse | null>(
    null,
  );

  const categories = useMemo(() => {
    const result: Record<
      string,
      { name: string; subcategories: CategoryResponse[] }
    > = {};
    currentProject?.report_categories.forEach((c) => {
      const [top, sub] = c.id.split('.', 2);
      if (!result[top]) {
        result[top] = { name: c.id === top ? c.name : top, subcategories: [] };
      }
      if (sub) {
        result[top].subcategories.push({ id: sub, name: c.name });
      } else {
        result[top].name = c.name;
      }
    });
    return result;
  }, [currentProject]);

  useEffect(() => {
    setLoadingProjects(true);
    fetchProjects()
      .then((response) => {
        setProjects(response.projects);
        const url = new URL(document.location.href);
        const deferredProject = url.searchParams.get('project');
        if (deferredProject) {
          const project = response.projects.find(
            (p) => p.id.toString() === deferredProject,
          );
          if (project) {
            setCurrentProject(project);
            setSelected({
              project: project.id,
              version: null,
              category: null,
              subcategory: null,
            });
          }
        }
        setLoadingProjects(false);
      })
      .catch((error) => {
        console.error('Error fetching projects:', error);
        setProjects([]);
        setLoadingProjects(false);
      });
  }, []);

  useEffect(() => {
    if (selected.project === null) {
      return;
    }
    setLoadingProject(true);
    fetchProjectById(selected.project, selected.version)
      .then((response) => {
        setCurrentProject(response);
        setLoadingProject(false);
      })
      .catch((error) => {
        console.error('Error fetching project by ID:', error);
        setCurrentProject(null);
        setLoadingProject(false);
      });
  }, [selected.project, selected.version]);

  const url = new URL('/', window.location.origin);
  url.searchParams.append('mode', mode);
  if (currentProject) {
    if (useProjectId) {
      url.pathname = `/projects/${currentProject.id}`;
      if (selected.version) {
        url.searchParams.append('version', selected.version);
      }
    } else {
      url.pathname = `/${currentProject.owner}/${currentProject.repo}`;
      if (selected.version) {
        url.pathname += `/${selected.version || currentProject.default_version}`;
      }
    }
    if (selected.category) {
      const cat = selected.subcategory
        ? `${selected.category}.${selected.subcategory}`
        : selected.category;
      url.searchParams.append('category', cat);
    }
  }
  url.pathname += `.${format}`;
  if (mode === 'shield') {
    if (formatOptions.label) {
      url.searchParams.append('label', formatOptions.label);
    }
    if (formatOptions.labelColor) {
      url.searchParams.append('labelColor', formatOptions.labelColor);
    }
    if (formatOptions.color) {
      url.searchParams.append('color', formatOptions.color);
    }
    if (formatOptions.style) {
      url.searchParams.append('style', formatOptions.style);
    }
    if (formatOptions.measure) {
      url.searchParams.append('measure', formatOptions.measure);
    }
  }

  const updateMode = (newMode: string) => {
    setMode(newMode);
    setFormat(formatsByMode[newMode][0]);
    setFormatOptions({});
  };

  const updateFormat = (newFormat: string) => {
    setFormat(newFormat);
  };

  let modeOptions = null;
  if (mode === 'shield') {
    modeOptions = (
      <>
        <div className="grid">
          <label>
            Measure
            <select
              name="measure"
              value={formatOptions.measure || ''}
              onChange={(e) =>
                setFormatOptions((existing: any) => ({
                  ...existing,
                  measure: e.target.value,
                }))
              }
            >
              <option value="">Default</option>
              <option value="fuzzy_match_percent">Fuzzy Match (Percent)</option>
              <option value="matched_code_percent">
                Matched Code (Percent)
              </option>
              <option value="matched_code_bytes">Matched Code (Bytes)</option>
              <option value="matched_code_size">Matched Code (Size)</option>
              <option value="matched_data_percent">
                Matched Data (Percent)
              </option>
              <option value="matched_data_bytes">Matched Data (Bytes)</option>
              <option value="matched_data_size">Matched Data (Size)</option>
              <option value="matched_functions">
                Matched Functions (Count)
              </option>
              <option value="matched_functions_percent">
                Matched Functions (Percent)
              </option>
              <option value="complete_code_percent">
                Linked Code (Percent)
              </option>
              <option value="complete_code_bytes">Linked Code (Bytes)</option>
              <option value="complete_code_size">Linked Code (Size)</option>
              <option value="complete_data_percent">
                Linked Data (Percent)
              </option>
              <option value="complete_data_bytes">Linked Data (Bytes)</option>
              <option value="complete_data_size">Linked Data (Size)</option>
              <option value="complete_units">Linked Units (Count)</option>
              <option value="complete_units_percent">
                Linked Units (Percent)
              </option>
            </select>
          </label>
        </div>
        <div className="grid">
          <label>
            Style
            <select
              name="style"
              value={formatOptions.style || ''}
              onChange={(e) =>
                setFormatOptions((existing: any) => ({
                  ...existing,
                  style: e.target.value,
                }))
              }
            >
              <option value="">Default</option>
              <option value="flat">Flat</option>
              <option value="plastic">Plastic</option>
              <option value="flatsquare">Flat Square</option>
            </select>
          </label>
          <label>
            Color
            <div className={styles.clearContainer}>
              <input
                name="color"
                type="color"
                value={formatOptions.color || ''}
                onChange={(e) =>
                  setFormatOptions((existing: any) => ({
                    ...existing,
                    color: e.target.value,
                  }))
                }
              />
              <button
                className="secondary icon-cancel"
                onClick={() =>
                  setFormatOptions((existing: any) => ({
                    ...existing,
                    color: null,
                  }))
                }
              />
            </div>
          </label>
        </div>
        <div className="grid">
          <label>
            Label
            <input
              name="label"
              type="text"
              value={formatOptions.label || ''}
              onChange={(e) =>
                setFormatOptions((existing: any) => ({
                  ...existing,
                  label: e.target.value,
                }))
              }
            />
          </label>
          <label>
            Label Color
            <div className={styles.clearContainer}>
              <input
                name="labelColor"
                type="color"
                value={formatOptions.labelColor || ''}
                onChange={(e) =>
                  setFormatOptions((existing: any) => ({
                    ...existing,
                    labelColor: e.target.value,
                  }))
                }
              />
              <button
                className="secondary icon-cancel"
                onClick={() =>
                  setFormatOptions((existing: any) => ({
                    ...existing,
                    labelColor: null,
                  }))
                }
              />
            </div>
          </label>
        </div>
      </>
    );
  }

  return (
    <div>
      <div className="grid">
        <label>
          Project
          <select
            name="project"
            value={selected.project || ''}
            disabled={loadingProjects}
            onChange={(e) =>
              setSelected({
                project: e.target.value,
                version: null,
                category: null,
                subcategory: null,
              })
            }
          >
            <option value="">Select a project</option>
            {projects.map((project) => (
              <option key={project.id} value={project.id}>
                {project.name}
              </option>
            ))}
          </select>
        </label>
        <label>
          Version
          <select
            name="version"
            value={selected.version || ''}
            disabled={loadingProject || !currentProject}
            onChange={(e) =>
              setSelected((existing) => ({
                project: existing.project,
                version: e.target.value,
                category: null,
                subcategory: null,
              }))
            }
          >
            <option value="">Default</option>
            {currentProject?.report_versions.map((version) => (
              <option key={version} value={version}>
                {version}
              </option>
            ))}
          </select>
        </label>
        <label>
          Category
          <select
            name="category"
            value={selected.category || ''}
            disabled={loadingProject || !currentProject}
            onChange={(e) =>
              setSelected((existing) => ({
                project: existing.project,
                version: existing.version,
                category: e.target.value || null,
                subcategory: null,
              }))
            }
          >
            <option value="">Default</option>
            {Object.entries(categories).map(([id, info]) => (
              <option key={id} value={id}>
                {info.name}
              </option>
            ))}
          </select>
        </label>
      </div>
      {selected.category &&
        categories[selected.category]?.subcategories.length > 0 && (
          <div className="grid">
            <label>
              Subcategory
              <select
                name="subcategory"
                value={selected.subcategory || ''}
                disabled={loadingProject || !currentProject}
                onChange={(e) =>
                  setSelected((existing) => ({
                    ...existing,
                    subcategory: e.target.value || null,
                  }))
                }
              >
                <option value="">Default</option>
                {categories[selected.category].subcategories.map((sub) => (
                  <option key={sub.id} value={sub.id}>
                    {sub.name}
                  </option>
                ))}
              </select>
            </label>
          </div>
        )}
      <div className="grid">
        <label>
          Mode
          <select
            name="mode"
            value={mode}
            onChange={(e) => updateMode(e.target.value)}
          >
            <option value="overview">Overview (Project info & measures)</option>
            <option value="measures">Measures (Simple report data)</option>
            <option value="report">
              Report (Full report data, very large!)
            </option>
            <option value="shield">Shield (Progress badge)</option>
            <option value="history">History (Historical report data)</option>
          </select>
        </label>
        <label>
          Format
          <select
            name="format"
            value={format}
            onChange={(e) => updateFormat(e.target.value)}
          >
            {formatsByMode[mode].map((format) => {
              switch (format) {
                case 'json':
                  return (
                    <option key={format} value={format}>
                      JSON
                    </option>
                  );
                case 'binpb':
                  return (
                    <option key={format} value={format}>
                      Protobuf
                    </option>
                  );
                case 'svg':
                  return (
                    <option key={format} value={format}>
                      SVG
                    </option>
                  );
                case 'png':
                  return (
                    <option key={format} value={format}>
                      PNG
                    </option>
                  );
              }
            })}
          </select>
        </label>
      </div>
      <div className="grid">
        <label>
          <input
            type="checkbox"
            name="useProjectId"
            checked={useProjectId}
            onChange={(e) => setUseProjectId(e.target.checked)}
          />
          Use project ID instead of repository name
        </label>
      </div>
      <hr />
      {modeOptions}
      {currentProject && <ApiPreview url={url.toString()} format={format} />}
    </div>
  );
};

const formatsByMode: Record<string, string[]> = {
  overview: ['json', 'svg', 'png'],
  measures: ['json', 'binpb'],
  report: ['json', 'binpb'],
  shield: ['svg', 'png', 'json'],
  history: ['json'],
};

const prettifyJson = (data: string): string => {
  let parsed: any;
  try {
    parsed = JSON.parse(data);
  } catch (error) {
    // Return the original data if parsing fails
    return data;
  }
  return JSON.stringify(parsed, null, 2);
};

const ApiPreview = ({
  url,
  format,
}: { url: string | null; format: string }) => {
  type ResponseData = {
    data: string;
    format: string;
  };
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [response, setResponse] = useState<ResponseData | null>(null);

  // biome-ignore lint/correctness/useExhaustiveDependencies:
  useEffect(() => {
    setResponse(null);
  }, [url]);

  const fetchData = async () => {
    if (!url) {
      return;
    }
    setLoading(true);
    setError(null);
    try {
      const response = await fetchApiUrl(url);
      setResponse({ data: response, format });
    } catch (error) {
      console.error('Error fetching API URL:', error);
      setError('Failed to fetch data');
    } finally {
      setLoading(false);
    }
  };

  const stringified = useMemo(() => {
    if (!response) {
      return '';
    }
    const maxLength = 250000; // 250 KB
    let stringified: string;
    if (response.format === 'json') {
      stringified = prettifyJson(response.data);
    } else if (response.format === 'binpb') {
      stringified = `// Binary protobuf size ${formatBytes(response.data.length)}`;
    } else {
      stringified = response.data;
    }
    if (stringified.length > maxLength) {
      return `// Response was ${formatBytes(response.data.length)}, preview truncated to ${formatBytes(maxLength, 0)}\n${stringified.slice(0, maxLength)}`;
    }
    return stringified;
  }, [response]);

  if (!url) {
    return null;
  }

  let preview = null;
  if (response) {
    if (response.format === 'json' || response.format === 'binpb') {
      preview = (
        <CodeBlock
          value={stringified}
          originalValue={response.data}
          language="json"
        />
      );
    }
  }
  if (error) {
    preview = <CodeBlock value={error} language="plaintext" />;
  }

  if (format === 'svg' || format === 'png') {
    preview = <img src={url} alt="Preview" />;
  } else if (format === 'json' || format === 'binpb') {
    preview = (
      <>
        <div className="grid">
          <button
            className="secondary"
            onClick={fetchData}
            disabled={loading || (format !== 'json' && format !== 'binpb')}
          >
            Preview
          </button>
        </div>
        <br />
        {preview}
      </>
    );
  }

  return (
    <>
      <CodeBlock value={url.toString()} language="plaintext" />
      {preview}
    </>
  );
};

function formatBytes(bytes: number, decimals = 2): string {
  if (bytes === 0) return '0 B';

  const k = 1000;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

  const sign = bytes < 0 ? '-' : '';
  const absoluteBytes = Math.abs(bytes);
  const i = Math.min(
    Math.floor(Math.log10(absoluteBytes) / Math.log10(k)),
    sizes.length - 1,
  );

  const value = absoluteBytes / k ** i;
  return `${sign}${value.toFixed(dm)} ${sizes[i]}`;
}

const rootNode = document.getElementById('root');
if (rootNode) {
  const root = createRoot(rootNode);
  root.render(
    <StrictMode>
      <ProjectForm />
    </StrictMode>,
  );
}
