"""Application settings. Values come from the environment (see .app.env)."""
from pydantic_settings import BaseSettings, SettingsConfigDict

PLACEHOLDER_PREFIX = "REPLACE_WITH_"


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=None, extra="ignore")

    app_env: str = "production"
    database_url: str = "postgresql+psycopg://dfp:dfp@db:5432/dfp"

    # The site's own origin, e.g. https://dutyfreeprofessor.demoing.info. Every
    # absolute URL the server emits (canonical, sitemap, JSON-LD @id) is built
    # from it. Outside production an unset value falls back to the request's
    # Host header; in production it does not, because a header any client can
    # send must never decide what a crawler is told the canonical site is.
    public_base_url: str = ""

    # The runtime home of the files the app writes and serves (Stream AW3): the picture
    # derivatives under `<uploads_dir>/images/<level>/` are mounted at /uploads/images. The
    # container bind-mounts the workspace's uploads/ here; a local run points it anywhere.
    uploads_dir: str = "/srv/uploads"

    @property
    def is_production(self) -> bool:
        return self.app_env == "production"

    @property
    def collect_freeze_file(self):
        """The deploy freeze marker (Stream AW4): `deploy` writes `D3 <iso> by <user>` here before
        its gate check and removes it after health; while it exists no collector starts (a Start
        from the page answers COLLECT_FROZEN, `app.cli collect` exits 3 without --ignore-freeze).
        Under the uploads directory because that is the one path both the container and a local
        run can write."""
        from pathlib import Path

        return Path(self.uploads_dir) / ".collect-freeze"

    # IndexNow key (app/services/indexnow.py): any 8-128 character hex/alnum
    # string we mint once; served at /<key>.txt so the engines can check it.
    # Not a secret, but it lives in .app.env with the rest of the environment.
    indexnow_key: str = ""

    @property
    def has_indexnow(self) -> bool:
        key = self.indexnow_key
        return bool(key) and not key.startswith(PLACEHOLDER_PREFIX) and key.isalnum()

    # Feature switches. Code defaults describe the full product; the demo (or
    # any other audience) is a configuration in .app.env, never a code change.
    # Injected into the SPA shell as window.__DFP_FLAGS__ at startup.
    feature_my_airports: bool = True     # airport picker + route scoping + /savings
    feature_featured_home: bool = True   # the algorithmic "biggest savings" cards
    feature_rich_cards: bool = True      # per-shop price bars inside product cards
    feature_teasers: bool = True
    # The product line pages (Stream K5; plan W1): /products/<line-slug>[?variant&airports]. False
    # restores the variant pages and their addresses until Cannes; nothing in the data changes.
    line_pages: bool = True
    feature_category_at_airport: bool = False  # 'whisky at Heathrow' pages: built, held for a later quote (rian, 13 Sep)
    feature_client_todos: bool = True   # the client to-do page and its uploads; OFF in production until the app login exists         # editorial/alerts/newsletter/coming-soon slots

    @property
    def spa_flags(self) -> dict[str, bool]:
        return {
            "myAirports": self.feature_my_airports,
            "clientTodos": self.feature_client_todos,
            "featuredHome": self.feature_featured_home,
            "richCards": self.feature_rich_cards,
            "teasers": self.feature_teasers,
            "linePages": self.line_pages,
            # Members-only mode: the SPA draws nothing but the sign-in pages to an anonymous
            # visitor (no header, footer or navigation), so the shape of the site is not shown
            # before launch. The server refuses the data regardless (services/access.py).
            "siteOpen": self.site_open,
            # False on live: the SPA neither links nor routes the development surfaces there,
            # matching the server, which answers 404 for them (services/access.py). It is a
            # second placement on top of the classes, never the public gate.
            "developmentSurfaces": not self.is_live,
        }

    # The site's social profiles, handed over by the client (the /todo item) and
    # set in .app.env; empty means the profile does not exist yet. One home for
    # them: the shell's window.__DFP_SOCIAL__ (header and footer links) and the
    # Organization markup's sameAs both read `social_profiles`, so the page and
    # the structured data can never disagree, and no dead icon ships.
    social_instagram_url: str = ""
    social_youtube_url: str = ""

    @property
    def social_profiles(self) -> dict[str, str]:
        """{network: url} for the profiles that exist: https only, no placeholders."""
        out: dict[str, str] = {}
        for network, url in (("instagram", self.social_instagram_url), ("youtube", self.social_youtube_url)):
            url = (url or "").strip()
            if url.startswith("https://") and not url.startswith(f"https://{PLACEHOLDER_PREFIX}") and PLACEHOLDER_PREFIX not in url:
                out[network] = url
        return out

    # The account system (services/accounts.py, docs/ACCOUNTS.md). The owner is code, not
    # a row: the username in ACCOUNT_OWNER is the super admin. Unset or a placeholder means
    # nobody is owner (the kit would otherwise default one silently), the startup log says
    # CRITICAL and the site stays up. SITE_ACCESS is `members` (the code default: every
    # storefront read needs a signed-in account) or `public` (the go-live flip, E7's step).
    account_owner: str = ""
    site_access: str = "members"

    # Which environment this container is (docs/ACCOUNTS.md, the route policy). `live` is the
    # production host; anything else, unset included, is staging. APP_ENV cannot answer this:
    # the compose file sets it to `production` on both machines, so it tells them apart not at
    # all. SITE_ROLE is not the public gate either -- SITE_ACCESS decides what an anonymous
    # visitor may read. It decides whether the surfaces built for the build exist at all.
    site_role: str = "staging"

    @property
    def is_live(self) -> bool:
        """Only the literal `live` is live. An unset or misspelt line reads as staging, which
        shows MORE to a signed-in account and nothing more to the public (`site_open` still
        gates that), so a live host must carry the line and it is checked by behaviour:
        `/api/health` prints `"role":"live"`."""
        return (self.site_role or "").strip().lower() == "live"

    @property
    def owner_username(self) -> str | None:
        name = (self.account_owner or "").strip().lower()
        if not name or name.startswith(PLACEHOLDER_PREFIX.lower()):
            return None
        return name

    @property
    def site_open(self) -> bool:
        """Fail closed: anything but the literal `public` keeps the site members-only."""
        return (self.site_access or "").strip().lower() == "public"

    # Mail for invites and resets (services/mail.py): `none` (the default; the CLI prints a
    # one-time link instead) or `resend` with RESEND_API_KEY and MAIL_FROM. A placeholder
    # reads as absent. The key belongs in an app-only env file, never a shared one.
    mail_provider: str = "none"
    mail_from: str = ""
    resend_api_key: str = ""

    @property
    def mail_ready(self) -> bool:
        key, sender = self.resend_api_key, self.mail_from
        return (bool(key) and not key.startswith(PLACEHOLDER_PREFIX)
                and bool(sender) and not sender.startswith(PLACEHOLDER_PREFIX) and "@" in sender)

    # The rendered-fetch sidecar (main/browser/), reachable only on the render
    # network. Empty means "no browser": render() raises FetchError, never falls
    # back to a text fetch that would read a shell page as a catalogue.
    browser_url: str = ""

    # Optional paid fetch service. Absent by default: we collect only from
    # sources that serve us openly (see .logs/planning/poc-dfp.md, F3-rev).
    fetch_service_key: str = ""

    @property
    def has_fetch_service(self) -> bool:
        """A staged placeholder must read as absent, not configured (coding.md)."""
        key = self.fetch_service_key
        return bool(key) and not key.startswith(PLACEHOLDER_PREFIX)


settings = Settings()
