import { invoke } from "@tauri-apps/api/core";
import { openUrl } from "@tauri-apps/plugin-opener";
import { create } from "zustand";
import { combine } from "zustand/middleware";
import type {
  PRFilter,
  PullRequest,
  PullRequestComment,
  PullRequestDetails,
  PullRequestFile,
} from "../types/github";
import {
  syncGitHubTokenFromAccount,
  type GitHubTokenSyncStatus,
} from "../services/github-token-service";

const PR_LIST_CACHE_TTL_MS = 5 * 60_000;
const PR_DETAILS_CACHE_TTL_MS = 120_000;
const AUTH_CACHE_TTL_MS = 2 * 60_000;

interface PRListCacheEntry {
  fetchedAt: number;
  prs: PullRequest[];
}

interface PRDetailsCacheEntry {
  fetchedAt: number;
  details: PullRequestDetails;
  diff?: string;
  files?: PullRequestFile[];
  comments?: PullRequestComment[];
  filesFetchedAt?: number;
  commentsFetchedAt?: number;
  contentFetchedAt?: number;
}

type GitHubAuthStatus = "authenticated" | "notAuthenticated";
type GitHubAccountStatus = "unknown" | "notSignedIn" | "notConnected" | "connected";

interface GitHubState {
  prs: PullRequest[];
  currentFilter: PRFilter;
  isLoading: boolean;
  error: string | null;
  activeRepoPath: string | null;
  isAuthenticated: boolean;
  isCheckingAuth: boolean;
  authStatus: GitHubAuthStatus;
  githubAccountStatus: GitHubAccountStatus;
  currentUser: string | null;
  // Selected PR state
  selectedPRNumber: number | null;
  selectedPRDetails: PullRequestDetails | null;
  selectedPRDiff: string | null;
  selectedPRFiles: PullRequestFile[];
  selectedPRComments: PullRequestComment[];
  isLoadingDetails: boolean;
  isLoadingContent: boolean;
  detailsError: string | null;
  contentError: string | null;
  prListCache: Record<string, PRListCacheEntry>;
  prDetailsCache: Record<string, PRDetailsCacheEntry>;
}

const initialState: GitHubState = {
  prs: [],
  currentFilter: "all",
  isLoading: false,
  error: null,
  activeRepoPath: null,
  isAuthenticated: false,
  isCheckingAuth: false,
  authStatus: "notAuthenticated" as GitHubAuthStatus,
  githubAccountStatus: "unknown" as GitHubAccountStatus,
  currentUser: null,
  // Selected PR state
  selectedPRNumber: null,
  selectedPRDetails: null,
  selectedPRDiff: null,
  selectedPRFiles: [],
  selectedPRComments: [],
  isLoadingDetails: false,
  isLoadingContent: false,
  detailsError: null,
  contentError: null,
  prListCache: {},
  prDetailsCache: {},
};

let prsRequestSeq = 0;
let authCheckedAt = 0;
const prDetailsRequestSeqByKey: Record<string, number> = {};
const prContentRequestSeqByKey: Record<string, number> = {};
const prDetailsInFlightByKey: Record<string, Promise<void> | undefined> = {};
const prContentInFlightByKey: Record<string, Promise<void> | undefined> = {};

function getPRListCacheKey(repoPath: string, filter: PRFilter): string {
  return `${repoPath}::${filter}`;
}

function getPRDetailsCacheKey(repoPath: string, prNumber: number): string {
  return `${repoPath}::${prNumber}`;
}

function isFresh(timestamp: number, ttlMs: number): boolean {
  return Date.now() - timestamp < ttlMs;
}

function getAccountStatus(syncStatus: GitHubTokenSyncStatus): GitHubAccountStatus {
  if (syncStatus === "synced") return "connected";
  if (syncStatus === "notSignedIn") return "notSignedIn";
  return "notConnected";
}

function normalizePullRequestFiles(files: unknown): PullRequestFile[] {
  if (!Array.isArray(files)) return [];

  return files
    .map((file) => {
      if (!file || typeof file !== "object") return null;
      const record = file as Record<string, unknown>;
      const path = typeof record.path === "string" ? record.path.trim() : "";
      if (!path) return null;

      return {
        path,
        additions: typeof record.additions === "number" ? record.additions : 0,
        deletions: typeof record.deletions === "number" ? record.deletions : 0,
      };
    })
    .filter((file): file is PullRequestFile => !!file);
}

function getStringValue(record: Record<string, unknown>, keys: string[]): string {
  for (const key of keys) {
    const value = record[key];
    if (typeof value === "string" && value.trim()) {
      return value;
    }
  }

  return "";
}

function normalizePullRequest(pr: PullRequest): PullRequest {
  const record = pr as PullRequest & Record<string, unknown>;
  const headRef = getStringValue(record, ["headRef", "headRefName", "head_ref"]);
  const baseRef = getStringValue(record, ["baseRef", "baseRefName", "base_ref"]);

  if (!headRef || !baseRef) {
    console.warn("GitHub PR list item is missing branch refs", {
      number: pr.number,
      title: pr.title,
      headRef,
      baseRef,
      rawKeys: Object.keys(record),
    });
  }

  return {
    ...pr,
    headRef,
    baseRef,
  };
}

function normalizePullRequestDetails(details: PullRequestDetails): PullRequestDetails {
  const record = details as PullRequestDetails & Record<string, unknown>;
  const statusChecks =
    details.statusChecks ??
    (Array.isArray(record.statusCheckRollup)
      ? (record.statusCheckRollup as PullRequestDetails["statusChecks"])
      : []);
  const linkedIssues =
    details.linkedIssues ??
    (Array.isArray(record.closingIssuesReferences)
      ? (record.closingIssuesReferences as PullRequestDetails["linkedIssues"])
      : []);

  return {
    ...details,
    headRef: getStringValue(record, ["headRef", "headRefName", "head_ref"]),
    baseRef: getStringValue(record, ["baseRef", "baseRefName", "base_ref"]),
    statusChecks,
    linkedIssues,
  };
}

export const useGitHubStore = create(
  combine(initialState, (set, get) => ({
    actions: {
      checkAuth: async (options?: { force?: boolean }) => {
        if (!options?.force && authCheckedAt && isFresh(authCheckedAt, AUTH_CACHE_TTL_MS)) {
          return;
        }

        set({ isCheckingAuth: true });

        try {
          const status = await invoke<GitHubAuthStatus>("github_check_auth");
          if (status === "authenticated") {
            const user = await invoke<string>("github_get_current_user");
            set({
              isAuthenticated: true,
              isCheckingAuth: false,
              authStatus: status,
              githubAccountStatus: "connected",
              currentUser: user,
              error: null,
            });
          } else {
            let githubAccountStatus = get().githubAccountStatus;

            if (status === "notAuthenticated") {
              try {
                const syncResult = await syncGitHubTokenFromAccount();
                githubAccountStatus = getAccountStatus(syncResult.status);

                if (syncResult.status === "synced") {
                  const syncedStatus = await invoke<GitHubAuthStatus>("github_check_auth");

                  if (syncedStatus === "authenticated") {
                    const user = await invoke<string>("github_get_current_user");
                    set({
                      isAuthenticated: true,
                      isCheckingAuth: false,
                      authStatus: syncedStatus,
                      githubAccountStatus,
                      currentUser: user,
                      error: null,
                    });
                    authCheckedAt = Date.now();
                    return;
                  }

                  set({
                    isAuthenticated: false,
                    isCheckingAuth: false,
                    authStatus: syncedStatus,
                    githubAccountStatus,
                    currentUser: null,
                  });
                  authCheckedAt = Date.now();
                  return;
                }
              } catch (error) {
                console.warn("Failed to sync GitHub account token:", error);
              }
            }

            set({
              isAuthenticated: false,
              isCheckingAuth: false,
              authStatus: status,
              githubAccountStatus,
              currentUser: null,
            });
          }
          authCheckedAt = Date.now();
        } catch {
          set({
            isAuthenticated: false,
            isCheckingAuth: false,
            authStatus: "notAuthenticated",
            githubAccountStatus: get().githubAccountStatus,
            currentUser: null,
          });
          authCheckedAt = Date.now();
        }
      },

      fetchPRs: async (repoPath: string, options?: { force?: boolean }) => {
        const { currentFilter } = get();
        const force = options?.force ?? false;
        const cacheKey = getPRListCacheKey(repoPath, currentFilter);
        const cached = get().prListCache[cacheKey];

        set({ activeRepoPath: repoPath, error: null });

        if (cached && !force && isFresh(cached.fetchedAt, PR_LIST_CACHE_TTL_MS)) {
          set({ prs: cached.prs, isLoading: false });
          return;
        }

        if (cached) {
          set({ prs: cached.prs, isLoading: true });
        } else {
          set({ isLoading: true });
        }

        const requestId = ++prsRequestSeq;

        try {
          const prsResponse = await invoke<PullRequest[]>("github_list_prs", {
            repoPath,
            filter: currentFilter,
          });
          const prs = prsResponse.map(normalizePullRequest);

          if (requestId !== prsRequestSeq) return;

          set((state) => ({
            prs,
            isLoading: false,
            prListCache: {
              ...state.prListCache,
              [cacheKey]: {
                fetchedAt: Date.now(),
                prs,
              },
            },
          }));
        } catch (err) {
          if (requestId !== prsRequestSeq) return;

          const message = err instanceof Error ? err.message : String(err);
          const isAuthError = /unauthorized|forbidden|401|403|credential|auth|token/i.test(message);

          if (isAuthError) {
            authCheckedAt = 0;
            set({
              isAuthenticated: false,
              currentUser: null,
              isLoading: false,
              error: null,
            });
            return;
          }

          set({
            error: message,
            isLoading: false,
            prs: cached?.prs ?? [],
          });
        }
      },

      setFilter: (filter: PRFilter) => {
        set({ currentFilter: filter });
      },

      setActiveRepoPath: (repoPath: string | null) => {
        set({ activeRepoPath: repoPath });
      },

      openPRInBrowser: async (repoPath: string, prNumber: number) => {
        try {
          const cacheKey = getPRDetailsCacheKey(repoPath, prNumber);
          const cachedUrl = get().prDetailsCache[cacheKey]?.details?.url;
          const url =
            cachedUrl ||
            (
              await invoke<PullRequestDetails>("github_get_pr_details", {
                repoPath,
                prNumber,
              })
            ).url;

          if (url.startsWith("https://github.com/")) {
            await openUrl(url);
          }
        } catch (err) {
          console.error("Failed to open PR:", err);
        }
      },

      checkoutPR: async (repoPath: string, prNumber: number) => {
        try {
          await invoke("github_checkout_pr", { repoPath, prNumber });
          window.dispatchEvent(new CustomEvent("git-status-changed"));
        } catch (err) {
          console.error("Failed to checkout PR:", err);
          throw err;
        }
      },

      selectPR: async (repoPath: string, prNumber: number, options?: { force?: boolean }) => {
        const force = options?.force ?? false;
        const cacheKey = getPRDetailsCacheKey(repoPath, prNumber);
        const cached = get().prDetailsCache[cacheKey];
        const hasFreshDetails =
          cached && !force && isFresh(cached.fetchedAt, PR_DETAILS_CACHE_TTL_MS);

        if (!force && prDetailsInFlightByKey[cacheKey]) {
          await prDetailsInFlightByKey[cacheKey];
          return;
        }

        if (hasFreshDetails) {
          set({
            selectedPRNumber: prNumber,
            selectedPRDetails: cached.details,
            selectedPRDiff: cached.diff ?? null,
            selectedPRFiles: cached.files ?? [],
            selectedPRComments: cached.comments ?? [],
            isLoadingDetails: false,
            detailsError: null,
            contentError: null,
          });
          return;
        }

        if (cached) {
          set({
            selectedPRNumber: prNumber,
            selectedPRDetails: cached.details,
            selectedPRDiff: cached.diff ?? null,
            selectedPRFiles: cached.files ?? [],
            selectedPRComments: cached.comments ?? [],
            isLoadingDetails: true,
            detailsError: null,
            contentError: null,
          });
        } else {
          set({
            selectedPRNumber: prNumber,
            selectedPRDetails: null,
            selectedPRDiff: null,
            selectedPRFiles: [],
            selectedPRComments: [],
            isLoadingDetails: true,
            detailsError: null,
            contentError: null,
          });
        }

        const requestId = (prDetailsRequestSeqByKey[cacheKey] ?? 0) + 1;
        prDetailsRequestSeqByKey[cacheKey] = requestId;

        const run = (async () => {
          try {
            const detailsResponse = await invoke<PullRequestDetails>("github_get_pr_details", {
              repoPath,
              prNumber,
            });
            const details = normalizePullRequestDetails(detailsResponse);

            if (requestId !== prDetailsRequestSeqByKey[cacheKey]) return;

            set((state) => ({
              selectedPRDetails: details,
              isLoadingDetails: false,
              detailsError: null,
              contentError: null,
              prDetailsCache: {
                ...state.prDetailsCache,
                [cacheKey]: {
                  ...state.prDetailsCache[cacheKey],
                  fetchedAt: Date.now(),
                  details,
                },
              },
            }));
          } catch (err) {
            if (requestId !== prDetailsRequestSeqByKey[cacheKey]) return;

            set({
              detailsError: err instanceof Error ? err.message : String(err),
              isLoadingDetails: false,
            });
          } finally {
            delete prDetailsInFlightByKey[cacheKey];
          }
        })();

        prDetailsInFlightByKey[cacheKey] = run;
        await run;
      },

      fetchPRContent: async (
        repoPath: string,
        prNumber: number,
        options?: { force?: boolean; mode?: "files" | "comments" | "full" },
      ) => {
        const force = options?.force ?? false;
        const mode = options?.mode ?? "full";
        const needsFiles = mode === "full" || mode === "files";
        const needsComments = mode === "full" || mode === "comments";
        const cacheKey = getPRDetailsCacheKey(repoPath, prNumber);
        const inFlightKey = `${cacheKey}::${mode}`;
        const cached = get().prDetailsCache[cacheKey];
        const filesFetchedAt = cached?.filesFetchedAt ?? cached?.contentFetchedAt;
        const commentsFetchedAt = cached?.commentsFetchedAt ?? cached?.contentFetchedAt;

        if (!force && prContentInFlightByKey[inFlightKey]) {
          await prContentInFlightByKey[inFlightKey];
          return;
        }

        const hasFreshFiles =
          filesFetchedAt &&
          cached.diff !== undefined &&
          cached.files !== undefined &&
          isFresh(filesFetchedAt, PR_DETAILS_CACHE_TTL_MS);
        const hasFreshComments =
          commentsFetchedAt &&
          cached.comments !== undefined &&
          isFresh(commentsFetchedAt, PR_DETAILS_CACHE_TTL_MS);
        const hasFreshContent =
          !force && (!needsFiles || !!hasFreshFiles) && (!needsComments || !!hasFreshComments);

        if (hasFreshContent) {
          const current = get();
          set({
            selectedPRDiff: needsFiles ? (cached?.diff ?? null) : current.selectedPRDiff,
            selectedPRFiles: needsFiles ? (cached?.files ?? []) : current.selectedPRFiles,
            selectedPRComments: needsComments
              ? (cached?.comments ?? [])
              : current.selectedPRComments,
            isLoadingContent: false,
            contentError: null,
          });
          return;
        }

        const current = get();
        const hasCachedRequestedData =
          (needsFiles && (cached?.diff !== undefined || cached?.files !== undefined)) ||
          (needsComments && cached?.comments !== undefined);

        if (hasCachedRequestedData) {
          set({
            selectedPRDiff: needsFiles ? (cached?.diff ?? null) : current.selectedPRDiff,
            selectedPRFiles: needsFiles ? (cached?.files ?? []) : current.selectedPRFiles,
            selectedPRComments: needsComments
              ? (cached?.comments ?? [])
              : current.selectedPRComments,
            isLoadingContent: true,
            contentError: null,
          });
        } else {
          set({
            selectedPRDiff: needsFiles ? null : current.selectedPRDiff,
            selectedPRFiles: needsFiles ? [] : current.selectedPRFiles,
            selectedPRComments: needsComments ? [] : current.selectedPRComments,
            isLoadingContent: true,
            contentError: null,
          });
        }

        const shouldFetchFiles = needsFiles && (!!force || !hasFreshFiles);
        const shouldFetchComments = needsComments && (!!force || !hasFreshComments);
        const requestId = (prContentRequestSeqByKey[cacheKey] ?? 0) + 1;
        prContentRequestSeqByKey[cacheKey] = requestId;

        const run = (async () => {
          try {
            const [diff, files, comments] = await Promise.all([
              shouldFetchFiles
                ? invoke<string>("github_get_pr_diff", { repoPath, prNumber })
                : Promise.resolve(undefined),
              shouldFetchFiles
                ? invoke<PullRequestFile[]>("github_get_pr_files", { repoPath, prNumber })
                : Promise.resolve(undefined),
              shouldFetchComments
                ? invoke<PullRequestComment[]>("github_get_pr_comments", { repoPath, prNumber })
                : Promise.resolve(undefined),
            ]);

            if (requestId !== prContentRequestSeqByKey[cacheKey]) return;

            const normalizedFiles = shouldFetchFiles ? normalizePullRequestFiles(files) : undefined;

            set((state) => {
              const now = Date.now();
              const baseDetails =
                state.prDetailsCache[cacheKey]?.details ??
                (state.selectedPRNumber === prNumber ? state.selectedPRDetails : null);
              const currentEntry = state.prDetailsCache[cacheKey];

              return {
                selectedPRDiff: needsFiles
                  ? shouldFetchFiles
                    ? (diff ?? null)
                    : state.selectedPRDiff
                  : state.selectedPRDiff,
                selectedPRFiles: needsFiles
                  ? shouldFetchFiles
                    ? (normalizedFiles ?? [])
                    : state.selectedPRFiles
                  : state.selectedPRFiles,
                selectedPRComments: needsComments
                  ? shouldFetchComments
                    ? (comments ?? [])
                    : state.selectedPRComments
                  : state.selectedPRComments,
                isLoadingContent: false,
                contentError: null,
                prDetailsCache: baseDetails
                  ? {
                      ...state.prDetailsCache,
                      [cacheKey]: {
                        ...(currentEntry ?? {
                          fetchedAt: now,
                          details: baseDetails,
                        }),
                        diff: shouldFetchFiles ? diff : currentEntry?.diff,
                        files: shouldFetchFiles ? normalizedFiles : currentEntry?.files,
                        comments: shouldFetchComments ? comments : currentEntry?.comments,
                        filesFetchedAt: shouldFetchFiles ? now : currentEntry?.filesFetchedAt,
                        commentsFetchedAt: shouldFetchComments
                          ? now
                          : currentEntry?.commentsFetchedAt,
                        contentFetchedAt:
                          shouldFetchFiles || shouldFetchComments
                            ? now
                            : currentEntry?.contentFetchedAt,
                      },
                    }
                  : state.prDetailsCache,
              };
            });
          } catch (err) {
            if (requestId !== prContentRequestSeqByKey[cacheKey]) return;

            set({
              contentError: err instanceof Error ? err.message : String(err),
              isLoadingContent: false,
            });
          } finally {
            delete prContentInFlightByKey[inFlightKey];
          }
        })();

        prContentInFlightByKey[inFlightKey] = run;
        await run;
      },

      deselectPR: () => {
        set({
          selectedPRNumber: null,
          selectedPRDetails: null,
          selectedPRDiff: null,
          selectedPRFiles: [],
          selectedPRComments: [],
          isLoadingDetails: false,
          isLoadingContent: false,
          detailsError: null,
          contentError: null,
        });
      },

      reset: () => {
        set(initialState);
      },
    },
  })),
);
