import type { Hono } from "hono";
import { z } from "zod";
import { getSettings } from "../settings-manager.js";
import { linearCache } from "../linear-cache.js";
import * as sessionLinearIssues from "../session-linear-issues.js";
import * as linearProjectManager from "../linear-project-manager.js";
import { parseBody } from "../validation.js";

// ─── Zod Schemas ──────────────────────────────────────────────────

const linearIssueAssocSchema = z.object({
  id: z.string().min(1, "id is required"),
  identifier: z.string().min(1, "identifier is required"),
  title: z.string().min(1, "title is required"),
  url: z.string().min(1, "url is required"),
  description: z.string().optional().default(""),
  branchName: z.string().optional().default(""),
  priorityLabel: z.string().optional().default(""),
  stateName: z.string().optional().default(""),
  stateType: z.string().optional().default(""),
  teamName: z.string().optional().default(""),
  teamKey: z.string().optional().default(""),
  teamId: z.string().optional().default(""),
  assigneeName: z.string().optional(),
  updatedAt: z.string().optional(),
});

const linearCommentSchema = z.object({
  body: z.string().min(1, "body is required").max(50_000),
});

const linearProjectMappingSchema = z.object({
  repoRoot: z.string().min(1, "repoRoot is required"),
  projectId: z.string().min(1, "projectId is required"),
  projectName: z.string().min(1, "projectName is required"),
});

const linearProjectMappingDeleteSchema = z.object({
  repoRoot: z.string().min(1, "repoRoot is required"),
});

function linearIssueStateCategory(issue: { stateType?: string; stateName?: string }): 0 | 1 | 2 {
  const stateType = (issue.stateType || "").trim().toLowerCase();
  const stateName = (issue.stateName || "").trim().toLowerCase();
  const isDone = stateType === "completed" || stateType === "canceled" || stateType === "cancelled"
    || stateName === "done" || stateName === "completed" || stateName === "canceled" || stateName === "cancelled";
  if (isDone) return 2;
  if (stateType === "started") return 1;
  return 0;
}

export function registerLinearRoutes(api: Hono): void {
  api.get("/linear/issues", async (c) => {
    const query = (c.req.query("query") || "").trim();
    const limitRaw = Number(c.req.query("limit") || "8");
    const limit = Math.min(20, Math.max(1, Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 8));
    if (!query) return c.json({ issues: [] });

    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    try {
      const cacheKey = `search:${query}:${limit}`;
      const issues = await linearCache.getOrFetch(cacheKey, 30_000, async () => {
        const response = await fetch("https://api.linear.app/graphql", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: linearApiKey,
          },
          body: JSON.stringify({
            query: `
              query ZencodeIssueSearch($term: String!, $first: Int!) {
                searchIssues(term: $term, first: $first) {
                  nodes {
                    id
                    identifier
                    title
                    description
                    url
                    branchName
                    priorityLabel
                    state { name type }
                    team { id key name }
                  }
                }
              }
            `,
            variables: { term: query, first: limit },
          }),
        }).catch((e: unknown) => {
          throw new Error(`Failed to connect to Linear: ${e instanceof Error ? e.message : String(e)}`);
        });

        const json = await response.json().catch(() => ({})) as {
          data?: {
            searchIssues?: {
              nodes?: Array<{
                id: string;
                identifier: string;
                title: string;
                description?: string | null;
                url: string;
                branchName?: string | null;
                priorityLabel?: string | null;
                state?: { name?: string | null; type?: string | null } | null;
                team?: { id?: string | null; key?: string | null; name?: string | null } | null;
              }>;
            };
          };
          errors?: Array<{ message?: string }>;
        };

        if (!response.ok || (json.errors && json.errors.length > 0)) {
          const firstError = json.errors?.[0]?.message || response.statusText || "Linear request failed";
          throw new Error(firstError);
        }

        return (json.data?.searchIssues?.nodes || []).map((issue) => ({
          id: issue.id,
          identifier: issue.identifier,
          title: issue.title,
          description: issue.description || "",
          url: issue.url,
          branchName: issue.branchName || "",
          priorityLabel: issue.priorityLabel || "",
          stateName: issue.state?.name || "",
          stateType: issue.state?.type || "",
          teamName: issue.team?.name || "",
          teamKey: issue.team?.key || "",
          teamId: issue.team?.id || "",
        }))
          .filter((issue) => linearIssueStateCategory(issue) !== 2)
          .sort((a, b) => linearIssueStateCategory(a) - linearIssueStateCategory(b));
      });

      return c.json({ issues });
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : "Linear request failed" }, 502);
    }
  });

  api.get("/linear/connection", async (c) => {
    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    try {
      const result = await linearCache.getOrFetch("connection", 300_000, async () => {
        const response = await fetch("https://api.linear.app/graphql", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: linearApiKey,
          },
          body: JSON.stringify({
            query: `
              query ZencodeLinearConnection {
                viewer { id name email }
                teams(first: 1) { nodes { id key name } }
              }
            `,
          }),
        }).catch((e: unknown) => {
          throw new Error(`Failed to connect to Linear: ${e instanceof Error ? e.message : String(e)}`);
        });

        const json = await response.json().catch(() => ({})) as {
          data?: {
            viewer?: { id?: string; name?: string | null; email?: string | null } | null;
            teams?: { nodes?: Array<{ id?: string; key?: string | null; name?: string | null }> } | null;
          };
          errors?: Array<{ message?: string }>;
        };

        if (!response.ok || (json.errors && json.errors.length > 0)) {
          const firstError = json.errors?.[0]?.message || response.statusText || "Linear request failed";
          throw new Error(firstError);
        }

        const firstTeam = json.data?.teams?.nodes?.[0];
        return {
          connected: true as const,
          viewerName: json.data?.viewer?.name || "",
          viewerEmail: json.data?.viewer?.email || "",
          teamName: firstTeam?.name || "",
          teamKey: firstTeam?.key || "",
        };
      });

      return c.json(result);
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : "Linear request failed" }, 502);
    }
  });

  // ─── Linear issue <-> session association ───────────────────────────

  api.put("/sessions/:id/linear-issue", async (c) => {
    const id = c.req.param("id");
    const parsed = await parseBody(c, linearIssueAssocSchema);
    if (!parsed.success) return parsed.response;
    const body = parsed.data;
    sessionLinearIssues.setLinearIssue(id, {
      id: body.id,
      identifier: body.identifier,
      title: body.title,
      description: body.description,
      url: body.url,
      branchName: body.branchName,
      priorityLabel: body.priorityLabel,
      stateName: body.stateName,
      stateType: body.stateType,
      teamName: body.teamName,
      teamKey: body.teamKey,
      teamId: body.teamId,
      assigneeName: body.assigneeName,
      updatedAt: body.updatedAt,
    });
    return c.json({ ok: true });
  });

  api.get("/sessions/:id/linear-issue", async (c) => {
    const id = c.req.param("id");
    const stored = sessionLinearIssues.getLinearIssue(id);
    if (!stored) return c.json({ issue: null });

    const refresh = c.req.query("refresh") === "true";
    if (!refresh) return c.json({ issue: stored });

    // Fetch fresh data from Linear API
    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) return c.json({ issue: stored });

    try {
      const cacheKey = `issue:${stored.id}`;
      const result = await linearCache.getOrFetch(cacheKey, 30_000, async () => {
        const response = await fetch("https://api.linear.app/graphql", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: linearApiKey,
          },
          body: JSON.stringify({
            query: `
              query ZencodeIssueFetch($id: String!) {
                issue(id: $id) {
                  id identifier title description url branchName priorityLabel
                  state { name type }
                  team { id key name }
                  comments(last: 5) {
                    nodes {
                      id body createdAt
                      user { id name displayName avatarUrl }
                    }
                  }
                  assignee { id name displayName avatarUrl }
                  labels { nodes { id name color } }
                }
              }
            `,
            variables: { id: stored.id },
          }),
        });

        const json = await response.json().catch(() => ({})) as {
          data?: {
            issue?: {
              id: string;
              identifier: string;
              title: string;
              description?: string | null;
              url: string;
              branchName?: string | null;
              priorityLabel?: string | null;
              state?: { name?: string | null; type?: string | null } | null;
              team?: { id?: string | null; key?: string | null; name?: string | null } | null;
              comments?: { nodes?: Array<{
                id: string;
                body: string;
                createdAt: string;
                user?: { name?: string | null; displayName?: string | null; avatarUrl?: string | null } | null;
              }> } | null;
              assignee?: { name?: string | null; displayName?: string | null; avatarUrl?: string | null } | null;
              labels?: { nodes?: Array<{ id: string; name: string; color: string }> } | null;
            } | null;
          };
          errors?: Array<{ message?: string }>;
        };

        return json.data?.issue ?? null;
      });

      if (result) {
        const updated = {
          id: result.id,
          identifier: result.identifier,
          title: result.title,
          description: result.description || "",
          url: result.url,
          branchName: result.branchName || "",
          priorityLabel: result.priorityLabel || "",
          stateName: result.state?.name || "",
          stateType: result.state?.type || "",
          teamName: result.team?.name || "",
          teamKey: result.team?.key || "",
          teamId: result.team?.id || "",
          assigneeName: result.assignee?.displayName || result.assignee?.name || "",
          updatedAt: new Date().toISOString(),
        };
        sessionLinearIssues.setLinearIssue(id, updated);
        return c.json({
          issue: updated,
          comments: (result.comments?.nodes || []).map((comment) => ({
            id: comment.id,
            body: comment.body,
            createdAt: comment.createdAt,
            userName: comment.user?.displayName || comment.user?.name || "Unknown",
            userAvatarUrl: comment.user?.avatarUrl || null,
          })),
          assignee: result.assignee ? {
            name: result.assignee.displayName || result.assignee.name || "",
            avatarUrl: result.assignee.avatarUrl || null,
          } : null,
          labels: (result.labels?.nodes || []).map((l) => ({
            id: l.id,
            name: l.name,
            color: l.color,
          })),
        });
      }
    } catch {
      // Fall through to return stored data on error
    }

    return c.json({ issue: stored });
  });

  api.delete("/sessions/:id/linear-issue", (c) => {
    const id = c.req.param("id");
    sessionLinearIssues.removeLinearIssue(id);
    return c.json({ ok: true });
  });

  api.post("/linear/issues/:issueId/comments", async (c) => {
    const issueId = c.req.param("issueId");
    const parsed = await parseBody(c, linearCommentSchema);
    if (!parsed.success) return parsed.response;
    const commentBody = parsed.data.body.trim();

    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    const response = await fetch("https://api.linear.app/graphql", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: linearApiKey,
      },
      body: JSON.stringify({
        query: `
          mutation ZencodeAddComment($issueId: String!, $body: String!) {
            commentCreate(input: { issueId: $issueId, body: $body }) {
              success
              comment { id body createdAt user { name displayName } }
            }
          }
        `,
        variables: { issueId, body: commentBody },
      }),
    }).catch((e: unknown) => {
      throw new Error(`Failed to connect to Linear: ${e instanceof Error ? e.message : String(e)}`);
    });

    const json = await response.json().catch(() => ({})) as {
      data?: {
        commentCreate?: {
          success?: boolean;
          comment?: {
            id: string;
            body: string;
            createdAt: string;
            user?: { name?: string | null; displayName?: string | null } | null;
          };
        };
      };
      errors?: Array<{ message?: string }>;
    };

    if (!response.ok || (json.errors && json.errors.length > 0)) {
      const firstError = json.errors?.[0]?.message || response.statusText || "Comment creation failed";
      return c.json({ error: firstError }, 502);
    }

    const result = json.data?.commentCreate;
    if (!result?.success || !result.comment) {
      return c.json({ error: "Comment creation failed" }, 502);
    }

    // Invalidate cached issue data so the next poll picks up the new comment
    linearCache.invalidate(`issue:${issueId}`);

    return c.json({
      ok: true,
      comment: {
        id: result.comment.id,
        body: result.comment.body,
        createdAt: result.comment.createdAt,
        userName: result.comment.user?.displayName || result.comment.user?.name || "You",
        userAvatarUrl: null,
      },
    });
  });

  api.get("/linear/states", async (c) => {
    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    try {
      const teams = await linearCache.getOrFetch("states", 300_000, async () => {
        const response = await fetch("https://api.linear.app/graphql", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: linearApiKey,
          },
          body: JSON.stringify({
            query: `
              query ZencodeWorkflowStates {
                teams {
                  nodes {
                    id
                    key
                    name
                    states {
                      nodes {
                        id
                        name
                        type
                      }
                    }
                  }
                }
              }
            `,
          }),
        }).catch((e: unknown) => {
          throw new Error(`Failed to connect to Linear: ${e instanceof Error ? e.message : String(e)}`);
        });

        const json = await response.json().catch(() => ({})) as {
          data?: {
            teams?: {
              nodes?: Array<{
                id?: string;
                key?: string | null;
                name?: string | null;
                states?: {
                  nodes?: Array<{
                    id?: string;
                    name?: string | null;
                    type?: string | null;
                  }>;
                };
              }>;
            };
          };
          errors?: Array<{ message?: string }>;
        };

        if (!response.ok || (json.errors && json.errors.length > 0)) {
          const firstError = json.errors?.[0]?.message || response.statusText || "Linear request failed";
          throw new Error(firstError);
        }

        return (json.data?.teams?.nodes || []).map((team) => ({
          id: team.id || "",
          key: team.key || "",
          name: team.name || "",
          states: (team.states?.nodes || []).map((state) => ({
            id: state.id || "",
            name: state.name || "",
            type: state.type || "",
          })),
        }));
      });

      return c.json({ teams });
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : "Linear request failed" }, 502);
    }
  });

  // ─── Linear projects ────────────────────────────────────────────────

  api.get("/linear/projects", async (c) => {
    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    try {
      const projects = await linearCache.getOrFetch("projects", 300_000, async () => {
        const response = await fetch("https://api.linear.app/graphql", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: linearApiKey,
          },
          body: JSON.stringify({
            query: `
              query ZencodeListProjects {
                projects(first: 50, orderBy: updatedAt) {
                  nodes { id name state }
                }
              }
            `,
          }),
        }).catch((e: unknown) => {
          throw new Error(`Failed to connect to Linear: ${e instanceof Error ? e.message : String(e)}`);
        });

        const json = await response.json().catch(() => ({})) as {
          data?: {
            projects?: { nodes?: Array<{ id?: string; name?: string | null; state?: string | null }> } | null;
          };
          errors?: Array<{ message?: string }>;
        };

        if (!response.ok || (json.errors && json.errors.length > 0)) {
          const firstError = json.errors?.[0]?.message || response.statusText || "Linear request failed";
          throw new Error(firstError);
        }

        return (json.data?.projects?.nodes || []).map((p) => ({
          id: p.id || "",
          name: p.name || "",
          state: p.state || "",
        }));
      });

      return c.json({ projects });
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : "Linear request failed" }, 502);
    }
  });

  // ─── Linear project issues (recent, non-done) ─────────────────────

  api.get("/linear/project-issues", async (c) => {
    const projectId = (c.req.query("projectId") || "").trim();
    const limitRaw = Number(c.req.query("limit") || "15");
    const limit = Math.min(50, Math.max(1, Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 15));
    if (!projectId) return c.json({ error: "projectId is required" }, 400);

    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    try {
      const cacheKey = `project-issues:${projectId}:${limit}`;
      const issues = await linearCache.getOrFetch(cacheKey, 60_000, async () => {
        const response = await fetch("https://api.linear.app/graphql", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: linearApiKey,
          },
          body: JSON.stringify({
            query: `
              query ZencodeProjectIssues($projectId: ID!, $first: Int!) {
                issues(
                  filter: {
                    project: { id: { eq: $projectId } }
                    state: { type: { nin: ["completed", "cancelled"] } }
                  }
                  orderBy: updatedAt
                  first: $first
                ) {
                  nodes {
                    id
                    identifier
                    title
                    description
                    url
                    priorityLabel
                    state { name type }
                    team { key name }
                    assignee { name }
                    updatedAt
                  }
                }
              }
            `,
            variables: { projectId, first: limit },
          }),
        }).catch((e: unknown) => {
          throw new Error(`Failed to connect to Linear: ${e instanceof Error ? e.message : String(e)}`);
        });

        const json = await response.json().catch(() => ({})) as {
          data?: {
            issues?: {
              nodes?: Array<{
                id: string;
                identifier: string;
                title: string;
                description?: string | null;
                url: string;
                priorityLabel?: string | null;
                state?: { name?: string | null; type?: string | null } | null;
                team?: { key?: string | null; name?: string | null } | null;
                assignee?: { name?: string | null } | null;
                updatedAt?: string | null;
              }>;
            };
          };
          errors?: Array<{ message?: string }>;
        };

        if (!response.ok || (json.errors && json.errors.length > 0)) {
          const firstError = json.errors?.[0]?.message || response.statusText || "Linear request failed";
          throw new Error(firstError);
        }

        return (json.data?.issues?.nodes || []).map((issue) => ({
          id: issue.id,
          identifier: issue.identifier,
          title: issue.title,
          description: issue.description || "",
          url: issue.url,
          priorityLabel: issue.priorityLabel || "",
          stateName: issue.state?.name || "",
          stateType: issue.state?.type || "",
          teamName: issue.team?.name || "",
          teamKey: issue.team?.key || "",
          assigneeName: issue.assignee?.name || "",
          updatedAt: issue.updatedAt || "",
        }))
          .filter((issue) => linearIssueStateCategory(issue) !== 2)
          .sort((a, b) => linearIssueStateCategory(a) - linearIssueStateCategory(b));
      });

      return c.json({ issues });
    } catch (e: unknown) {
      return c.json({ error: e instanceof Error ? e.message : "Linear request failed" }, 502);
    }
  });

  // ─── Linear project mappings ──────────────────────────────────────

  api.get("/linear/project-mappings", (c) => {
    const repoRoot = c.req.query("repoRoot");
    if (repoRoot) {
      const mapping = linearProjectManager.getMapping(repoRoot);
      return c.json({ mapping: mapping || null });
    }
    return c.json({ mappings: linearProjectManager.listMappings() });
  });

  api.put("/linear/project-mappings", async (c) => {
    const parsed = await parseBody(c, linearProjectMappingSchema);
    if (!parsed.success) return parsed.response;
    const body = parsed.data;
    const mapping = linearProjectManager.upsertMapping(body.repoRoot, {
      projectId: body.projectId,
      projectName: body.projectName,
    });
    return c.json({ mapping });
  });

  api.delete("/linear/project-mappings", async (c) => {
    const parsed = await parseBody(c, linearProjectMappingDeleteSchema);
    if (!parsed.success) return parsed.response;
    const removed = linearProjectManager.removeMapping(parsed.data.repoRoot);
    if (!removed) return c.json({ error: "Mapping not found" }, 404);
    return c.json({ ok: true });
  });

  api.post("/linear/issues/:id/transition", async (c) => {
    const issueId = c.req.param("id");
    if (!issueId) {
      return c.json({ error: "Issue ID is required" }, 400);
    }

    const settings = getSettings();
    const linearApiKey = settings.linearApiKey.trim();
    if (!linearApiKey) {
      return c.json({ error: "Linear API key is not configured" }, 400);
    }

    if (!settings.linearAutoTransition) {
      return c.json({ ok: true, skipped: true, reason: "auto_transition_disabled" });
    }

    const stateId = settings.linearAutoTransitionStateId.trim();
    if (!stateId) {
      return c.json({ ok: true, skipped: true, reason: "no_target_state_configured" });
    }

    try {
      const updateResponse = await fetch("https://api.linear.app/graphql", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: linearApiKey,
        },
        body: JSON.stringify({
          query: `
            mutation ZencodeTransitionIssue($issueId: String!, $stateId: String!) {
              issueUpdate(id: $issueId, input: { stateId: $stateId }) {
                success
                issue {
                  id
                  identifier
                  state { name type }
                }
              }
            }
          `,
          variables: { issueId, stateId },
        }),
      });

      const updateJson = await updateResponse.json().catch(() => ({})) as {
        data?: {
          issueUpdate?: {
            success?: boolean;
            issue?: {
              id?: string;
              identifier?: string;
              state?: { name?: string; type?: string };
            };
          };
        };
        errors?: Array<{ message?: string }>;
      };

      if (!updateResponse.ok || (updateJson.errors && updateJson.errors.length > 0)) {
        const errMsg = updateJson.errors?.[0]?.message || updateResponse.statusText || "Failed to update issue state";
        return c.json({ error: errMsg }, 502);
      }

      const updatedIssue = updateJson.data?.issueUpdate?.issue;

      // Invalidate cached issue data so the next fetch picks up the new state
      linearCache.invalidate(`issue:${issueId}`);

      return c.json({
        ok: true,
        skipped: false,
        issue: {
          id: updatedIssue?.id || issueId,
          identifier: updatedIssue?.identifier || "",
          stateName: updatedIssue?.state?.name || "",
          stateType: updatedIssue?.state?.type || "",
        },
      });
    } catch (e: unknown) {
      const errMsg = e instanceof Error ? e.message : String(e);
      return c.json({ error: `Linear transition failed: ${errMsg}` }, 502);
    }
  });

}
