defmodule MusicLibraryWeb.Router do
  use MusicLibraryWeb, :router
  use ErrorTracker.Integrations.Plug

  import MusicLibraryWeb.Auth, only: [require_logged_in: 2, require_api_token: 2]
  import Oban.Web.Router

  # Content Security Policy: restricts resource loading to same-origin by default,
  # allows inline styles/scripts and Inter font from rsms.me, album art from
  # Last.fm CDN, Brave Search, and Cover Art Archive, WASM from jsdelivr (barcode-detector),
  # and prevents framing. In dev, allows LiveDebugger and Phoenix dev assets from 127.0.0.1.
  @dev_origins if(Mix.env() == :dev, do: " http://127.0.0.1:* ws://127.0.0.1:*", else: "")
  @img_origins "https://lastfm.freetls.fastly.net https://imgs.search.brave.com " <>
                 "https://coverartarchive.org https://archive.org " <>
                 "https://*.archive.org https://www.google.com https://*.gstatic.com"

  @csp_policy "default-src 'self'" <>
                "; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' #{@dev_origins}" <>
                "; style-src 'self' 'unsafe-inline' https://rsms.me#{@dev_origins}" <>
                "; font-src 'self' https://rsms.me" <>
                "; img-src 'self' data: blob: #{@img_origins}" <>
                "; worker-src 'self' blob:" <>
                "; connect-src 'self' https://fastly.jsdelivr.net#{@dev_origins}" <>
                "; frame-ancestors 'self'" <>
                "; base-uri 'self'"

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

    plug :put_secure_browser_headers, %{
      "content-security-policy" => @csp_policy
    }
  end

  pipeline :api do
    plug :accepts, ["json"]
    plug :require_api_token
  end

  pipeline :logged_in do
    plug :require_logged_in
  end

  scope "/", MusicLibraryWeb do
    pipe_through :browser

    get "/health", HealthController, :index
    get "/login", SessionController, :new
    post "/sessions/create", SessionController, :create

    get "/auth/last_fm/callback", LastFmController, :callback

    get "/public/assets/:transform_payload", AssetController, :show

    scope "/" do
      pipe_through :logged_in

      get "/backup", ArchiveController, :backup

      get "/assets/:transform_payload", AssetController, :show

      live_session :default,
        on_mount: [
          MusicLibraryWeb.Hooks.StaticAssets,
          MusicLibraryWeb.Hooks.GetTimezone,
          MusicLibraryWeb.Hooks.ShowToast
        ] do
        live "/", StatsLive.Index, :index

        live "/collection", CollectionLive.Index, :index
        live "/collection/import", CollectionLive.Index, :import
        live "/collection/scan", CollectionLive.Index, :barcode_scan
        live "/collection/:id/edit", CollectionLive.Index, :edit

        live "/collection/:id", CollectionLive.Show, :show
        live "/collection/:id/show/edit", CollectionLive.Show, :edit

        live "/wishlist", WishlistLive.Index, :index
        live "/wishlist/import", WishlistLive.Index, :import
        live "/wishlist/:id/edit", WishlistLive.Index, :edit

        live "/wishlist/:id", WishlistLive.Show, :show
        live "/wishlist/:id/show/edit", WishlistLive.Show, :edit

        live "/artists/:musicbrainz_id", ArtistLive.Show, :show
        live "/artists/:musicbrainz_id/import", ArtistLive.Show, :import
        live "/artists/:musicbrainz_id/edit", ArtistLive.Show, :edit

        live "/record-sets", RecordSetLive.Index, :index
        live "/record-sets/new", RecordSetLive.Index, :new
        live "/record-sets/:id/edit", RecordSetLive.Index, :edit
        live "/record-sets/:id/add-record", RecordSetLive.Index, :add_record

        live "/record-sets/:id", RecordSetLive.Show, :show
        live "/record-sets/:id/show/edit", RecordSetLive.Show, :edit
        live "/record-sets/:id/show/add-record", RecordSetLive.Show, :add_record

        live "/scrobble-rules", ScrobbleRulesLive.Index, :index
        live "/scrobble-rules/new", ScrobbleRulesLive.Index, :new
        live "/scrobble-rules/:id/edit", ScrobbleRulesLive.Index, :edit

        live "/online-store-templates", OnlineStoreTemplateLive.Index, :index
        live "/online-store-templates/new", OnlineStoreTemplateLive.Index, :new
        live "/online-store-templates/:id/edit", OnlineStoreTemplateLive.Index, :edit

        live "/scrobbled-tracks", ScrobbledTracksLive.Index, :index
        live "/scrobbled-tracks/:scrobbled_at_uts/edit", ScrobbledTracksLive.Index, :edit

        live "/scrobble", ScrobbleLive.Index, :index
        live "/scrobble/:release_id", ScrobbleLive.Show, :show

        live "/maintenance", MaintenanceLive.Index, :index
      end
    end
  end

  scope "/api", MusicLibraryWeb do
    pipe_through :api

    get "/collection/latest", CollectionController, :latest
    get "/collection/random", CollectionController, :random
    get "/collection/on_this_day", CollectionController, :on_this_day
    get "/collection", CollectionController, :index
    get "/assets/:transform_payload", AssetController, :show
    get "/backup", ArchiveController, :backup
  end

  if Application.compile_env(:music_library, :monitoring_routes) do
    import Phoenix.LiveDashboard.Router
    use ErrorTracker.Web, :router

    pipeline :dev_dashboard do
      plug :generate_csp_nonces
      plug :put_dev_csp
    end

    scope "/dev" do
      pipe_through [:browser, :logged_in, :dev_dashboard]

      live_dashboard "/dashboard",
        metrics: MusicLibraryWeb.Telemetry,
        metrics_history: {MusicLibraryWeb.Telemetry.Storage, :metrics_history, []},
        ecto_repos: [MusicLibrary.Repo, MusicLibrary.BackgroundRepo, MusicLibrary.TelemetryRepo],
        csp_nonce_assign_key: %{
          img: :img_nonce,
          style: :style_nonce,
          script: :script_nonce
        }

      oban_dashboard "/oban",
        csp_nonce_assign_key: %{
          img: :img_nonce,
          style: :style_nonce,
          script: :script_nonce
        }

      error_tracker_dashboard "/errors",
        csp_nonce_assign_key: %{
          img: :img_nonce,
          style: :style_nonce,
          script: :script_nonce
        }
    end
  end

  defp generate_csp_nonces(conn, _opts) do
    nonce = Base.encode64(:crypto.strong_rand_bytes(16), padding: false)

    conn
    |> assign(:img_nonce, nonce)
    |> assign(:style_nonce, nonce)
    |> assign(:script_nonce, nonce)
  end

  # Replaces the default CSP with a dev-specific policy that adds nonce-based
  # exceptions for scripts, styles, and images, allowing Phoenix dev tools
  # (live reload, debug toolbar) to load alongside the standard policy.
  defp put_dev_csp(conn, _opts) do
    nonce = conn.assigns[:script_nonce]

    csp =
      "default-src 'self'" <>
        "; script-src 'self' 'nonce-#{nonce}'" <>
        "; style-src 'self' 'unsafe-inline' https://rsms.me" <>
        "; font-src 'self' data: https://rsms.me" <>
        "; img-src 'self' data: blob: 'nonce-#{nonce}' #{@img_origins}" <>
        "; connect-src 'self' https://fastly.jsdelivr.net" <>
        "; frame-ancestors 'self'" <>
        "; base-uri 'self'"

    delete_resp_header(conn, "content-security-policy")
    |> put_resp_header("content-security-policy", csp)
  end

  if Mix.env() == :dev do
    scope "/dev" do
      pipe_through [:browser, :logged_in]

      forward "/mailbox", Plug.Swoosh.MailboxPreview
    end
  end
end
