defmodule TarakanWeb.Router do
  use TarakanWeb, :router

  import TarakanWeb.AccountAuth

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {TarakanWeb.Layouts, :root}
    plug TarakanWeb.CurrentPath
    plug :protect_from_forgery

    plug :put_secure_browser_headers, %{
      "content-security-policy" =>
        "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; " <>
          "object-src 'none'; img-src 'self' data: https:; " <>
          "font-src 'self' data:; style-src 'self' 'unsafe-inline'; " <>
          "script-src 'self'; connect-src 'self' wss: ws:; form-action 'self'",
      "permissions-policy" => "camera=(), microphone=(), geolocation=()",
      "x-frame-options" => "SAMEORIGIN"
    }

    plug TarakanWeb.Plugs.CodeBrowserHeaders
    # CodeBrowserRateLimit runs after the session scope is fetched so
    # admins/moderators are exempt from the per-IP budget.
    plug :fetch_current_scope_for_account
    plug TarakanWeb.Plugs.CodeBrowserRateLimit
  end

  # OAuth starts/callbacks each force an upstream authorization redirect or
  # token-exchange POST; keep them on a small per-IP budget so a flood
  # cannot proxy abuse to GitHub/GitLab.
  pipeline :oauth_rate_limited do
    plug :rate_limit_oauth_ip
  end

  pipeline :api do
    plug :accepts, ["json"]
    plug TarakanWeb.Plugs.ApiRateLimit, mode: :ip
  end

  # Stripe webhooks: JSON in, no session/CSRF (signature-verified instead).
  pipeline :stripe_webhook do
    plug :accepts, ["json"]
  end

  scope "/webhooks", TarakanWeb.Webhooks do
    pipe_through :stripe_webhook

    post "/stripe", StripeController, :handle
  end

  pipeline :api_authenticated do
    plug :fetch_api_account
    plug TarakanWeb.Plugs.ApiRateLimit, mode: :actor
  end

  pipeline :seo do
    plug :accepts, ["xml", "txt"]
  end

  # Badges are embedded in READMEs on other hosts: no session, no CSRF, no
  # browser security headers, and an IP budget because anyone can hotlink one.
  pipeline :badge do
    plug :accepts, ["svg", "html"]
    # Its own bucket: a badge embedded in a popular README is high-volume and
    # cheap (cached), and must not drain the API budget for the same network.
    plug TarakanWeb.Plugs.ApiRateLimit, mode: :ip, bucket: :badge_ip, request_limit: 600
  end

  scope "/", TarakanWeb do
    pipe_through :seo

    get "/robots.txt", SEOController, :robots
    get "/.well-known/security.txt", SEOController, :security_txt
    get "/feeds/findings.xml", FeedController, :findings
    get "/feeds/infestations.xml", FeedController, :infestations
    get "/sitemap.xml", SEOController, :sitemap
    get "/sitemap/hubs.xml", SEOController, :sitemap_hubs
    get "/sitemap/repos/:page", SEOController, :sitemap_repos
    get "/sitemap/findings/:page", SEOController, :sitemap_findings
    get "/sitemap/jobs/:page", SEOController, :sitemap_jobs
  end

  scope "/", TarakanWeb do
    pipe_through [:browser, :oauth_rate_limited]

    get "/auth/github", GitHubAuthController, :request
    get "/auth/github/callback", GitHubAuthController, :callback
    get "/auth/gitlab", GitLabAuthController, :request
    get "/auth/gitlab/callback", GitLabAuthController, :callback
  end

  scope "/", TarakanWeb do
    pipe_through :browser

    # Permanent redirects from the pre-rename /patterns URLs.
    get "/patterns", InfestationRedirectController, :index
    get "/patterns/:pattern_key", InfestationRedirectController, :show

    live_session :public,
      on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
      live "/", RepositoryLive.Index, :index
      live "/policies/disclosure", PageLive, :disclosure
      live "/policies/content", PageLive, :content
      live "/pricing", PageLive, :pricing
      live "/services/disclosure", PageLive, :managed_disclosure
      live "/explore", ExploreLive, :index
      live "/infestations", InfestationLive.Index, :index
      live "/infestations/:pattern_key", InfestationLive.Show, :show
      live "/leaderboard", LeaderboardLive, :index
      live "/models", ModelAnalyticsLive, :index
      live "/jobs", JobsLive, :index
      live "/jobs/:id", ReviewTaskLive.Show, :show
      live "/agents", AgentsLive, :index
      live "/bounties", BountyLive.Index, :index
      # /bounties/new authenticates in-mount (it must precede the :public_id
      # route, and the public session runs first for the other two).
      live "/bounties/new", BountyLive.New, :new
      live "/bounties/:public_id", BountyLive.Show, :show
      live "/findings/:public_id", FindingLive.Show, :show
      live "/findings/:finding_ref/code", RepositoryCodeLive, :finding
    end
  end

  scope "/badges", TarakanWeb do
    pipe_through :badge

    # Hosted repositories are addressed without a host, remote ones with it.
    get "/:owner/:name", BadgeController, :show
    get "/:host/:owner/:name", BadgeController, :show
  end

  scope "/api", TarakanWeb.API do
    pipe_through [:api]

    post "/client-auth/start", ClientAuthController, :start
    post "/client-auth/exchange", ClientAuthController, :exchange

    # Public, read-only record (IP rate-limited; no token required)
    get "/findings/:public_id", FindingController, :show
    get "/infestations", InfestationController, :index
    get "/infestations/:pattern_key", InfestationController, :show
    get "/infestations/:pattern_key/vaccine.yaml", InfestationController, :vaccine
    get "/leaderboard", LeaderboardController, :index
  end

  scope "/api", TarakanWeb.API do
    pipe_through [:api, :api_authenticated]

    get "/repositories", RepositoryController, :index
    post "/repositories", RepositoryController, :create
    delete "/client-auth/session", ClientAuthController, :revoke

    # Detectors. Submitting one requires a worker that validated it against
    # known instances; reading one is free and lives on the public scope.
    post "/patterns/:code_pattern_key/rule", InfestationController, :put_rule

    # Jobs
    get "/jobs", WorkController, :queue
    get "/jobs/:id", WorkController, :show
    post "/jobs/:id/claim", WorkController, :claim
    post "/jobs/:id/claim/renew", WorkController, :renew
    delete "/jobs/:id/claim", WorkController, :release
    post "/jobs/:id/complete", WorkController, :complete
    get "/:host/:owner/:name/jobs", WorkController, :index

    # Reports + findings + checks
    get "/:host/:owner/:name/reports", ScanController, :index
    get "/:host/:owner/:name/memory", ScanController, :memory
    post "/:host/:owner/:name/reports", ScanController, :create
    post "/:host/:owner/:name/findings/:public_id/check", ScanController, :finding_verdict
    post "/:host/:owner/:name/findings/:public_id/severity", ScanController, :finding_severity
    post "/:host/:owner/:name/findings/:public_id/embedding", ScanController, :finding_embedding
    post "/:host/:owner/:name/reports/:id/check", ScanController, :verdict
  end

  # Enable LiveDashboard and Swoosh mailbox preview in development
  if Application.compile_env(:tarakan, :dev_routes) do
    # If you want to use the LiveDashboard in production, you should put
    # it behind authentication and allow only admins to access it.
    # If your application does not have an admins-only section yet,
    # you can use Plug.BasicAuth to set up some basic authentication
    # as long as you are also using SSL (which you should anyway).
    import Phoenix.LiveDashboard.Router

    scope "/dev" do
      pipe_through :browser

      live_dashboard "/dashboard", metrics: TarakanWeb.Telemetry
      forward "/mailbox", Plug.Swoosh.MailboxPreview
    end
  end

  ## Authentication routes

  scope "/", TarakanWeb do
    pipe_through [:browser, :require_authenticated_account]

    live_session :require_authenticated_account,
      on_mount: [{TarakanWeb.AccountAuth, :require_authenticated}, TarakanWeb.CurrentPath] do
      live "/accounts/settings", AccountLive.Settings, :edit
      live "/accounts/settings/confirm-email/:token", AccountLive.Settings, :confirm_email
      live "/accounts/settings/:section", AccountLive.Settings, :edit
      live "/accounts/billing", BillingLive, :show
      live "/alerts", AlertLive, :index
      live "/client/authorize/:user_code", ClientAuthorizationLive, :show
      live "/admin", AdminLive.Index, :index
      live "/admin/accounts/:id", AdminLive.Show, :show
      live "/repositories/new", RepositoryLive.New, :new
      live "/moderation/report", ModerationReportLive.New, :new
      live "/moderation/cases/:id", ModerationCaseLive.Show, :show
      live "/moderation/queue", ModerationQueueLive.Index, :index
    end

    # Checkout/portal exit points: plain POSTs that 303 to Stripe.
    post "/billing/checkout", BillingController, :checkout
    post "/billing/portal", BillingController, :portal

    post "/accounts/update-password", AccountSessionController, :update_password
  end

  scope "/", TarakanWeb do
    pipe_through [:browser]

    live_session :current_account,
      on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
      live "/accounts/register", AccountLive.Registration, :new
      live "/accounts/log-in", AccountLive.Login, :new
      live "/accounts/log-in/:token", AccountLive.Confirmation, :new
    end

    post "/accounts/log-in", AccountSessionController, :create
    delete "/accounts/log-out", AccountSessionController, :delete
  end

  ## Repository browsing
  #
  # Tarakan-hosted repositories live at GitHub-style bare paths
  # (/handle/name); remote repositories lead with their host's domain
  # (/github.com/owner/name, with legacy /github/... still resolving).
  #
  # Soundness: handles that would shadow a fixed route are reserved at
  # registration (Tarakan.Accounts.Account), handles can never contain a
  # dot, and host slugs are reserved handles - so a bare first segment is
  # classifiable in the mounts via Tarakan.Hosts.host_segment?/1. The bare
  # family is declared first so its literal segments win; when its first
  # segment is actually a host, the mount reinterprets the params. That
  # only happens for remote repositories literally named "code", "commits" or
  # "security" (their security tab is the one known-degraded corner).
  #
  # These wildcard routes must stay at the bottom of the router so they can
  # never shadow literal routes such as /accounts/log-in/:token.
  scope "/", TarakanWeb do
    pipe_through [:browser]

    live_session :repository_browser,
      on_mount: [{TarakanWeb.AccountAuth, :mount_current_scope}, TarakanWeb.CurrentPath] do
      # A bare single segment is a contributor profile (GitHub-style
      # /handle). Handles that would shadow a fixed route are reserved at
      # registration, so this can never swallow /accounts, /jobs, etc.
      live "/:handle", AccountLive.Profile, :show

      live "/:owner/:name", RepositoryCodeLive, :entry
      live "/:owner/:name/security", RepositoryLive.Show, :show
      live "/:owner/:name/commits", RepositoryCommitsLive, :index
      live "/:owner/:name/commits/:commit_sha", RepositoryCommitsLive, :show
      live "/:owner/:name/code", RepositoryCodeLive, :code_entry
      live "/:owner/:name/code/:commit_sha", RepositoryCodeLive, :show
      live "/:owner/:name/code/:commit_sha/*path", RepositoryCodeLive, :show

      live "/:host/:owner/:name", RepositoryCodeLive, :entry
      live "/:host/:owner/:name/security", RepositoryLive.Show, :show
      live "/:host/:owner/:name/commits", RepositoryCommitsLive, :index
      live "/:host/:owner/:name/commits/:commit_sha", RepositoryCommitsLive, :show
      live "/:host/:owner/:name/code", RepositoryCodeLive, :entry
      live "/:host/:owner/:name/code/:commit_sha", RepositoryCodeLive, :show
      live "/:host/:owner/:name/code/:commit_sha/*path", RepositoryCodeLive, :show
    end
  end

  @doc false
  def rate_limit_oauth_ip(conn, _opts) do
    key = TarakanWeb.Plugs.ClientIp.remote_ip_bucket(conn)

    if TarakanWeb.BrowserRateLimit.allowed?(:oauth_ip, key) do
      conn
    else
      conn
      |> put_resp_content_type("text/plain")
      |> send_resp(429, "too many requests")
      |> halt()
    end
  end
end
