from __future__ import annotations

import asyncio
import socket
from pathlib import Path

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import uvicorn

BASE_DIR = Path(__file__).resolve().parent


def create_app(
    get_state,
    trigger_refresh=None,
    dismiss_event=None,
    dismiss_all_events=None,
    fetch_repo=None,
    pull_repo=None,
    get_pipeline_jobs=None,
    pin_repo=None,
    unpin_repo=None,
    save_repo_order=None,
    exclude_repo=None,
    include_repo=None,
    rescan=None,
    _template_dir: Path | None = None,
    _static_dir: Path | None = None,
) -> FastAPI:
    """Factory returning a configured FastAPI app.

    get_state: callable returning a dict with workspace data, or None if not ready.
    trigger_refresh: optional async callable -- triggers a refresh cycle.
    dismiss_event: optional async callable(event_id: int) -- persists dismissal to DB.
    dismiss_all_events: optional async callable -- marks all events as seen.
    fetch_repo: optional async callable(repo_name: str) -- fetch a single repo.
    pull_repo: optional async callable(repo_name: str) -- pull a single repo.
    get_pipeline_jobs: optional async callable(repo_name, pipeline_id) -- get jobs.
    pin_repo: optional async callable(repo_name: str) -- pin a repo.
    unpin_repo: optional async callable(repo_name: str) -- unpin a repo.
    save_repo_order: optional async callable(order: list[str]) -- save card order.
    exclude_repo: optional async callable(repo_name: str) -- exclude a repo.
    include_repo: optional async callable(repo_name: str) -- re-include a repo.
    rescan: optional async callable -- rescan workspace for repos.
    _template_dir / _static_dir: override paths (used in tests).
    """
    app = FastAPI(title="project-hub")

    template_dir = _template_dir or BASE_DIR / "templates"
    static_dir = _static_dir or BASE_DIR / "static"

    templates = Jinja2Templates(directory=str(template_dir))
    app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")

    # --- full pages -----------------------------------------------------------

    @app.get("/")
    async def dashboard(request: Request):
        state = get_state()
        if state is None:
            return templates.TemplateResponse(request, "loading.html")
        return templates.TemplateResponse(request, "dashboard.html", {"state": state})

    @app.get("/mrs")
    async def mrs_page(request: Request):
        state = get_state()
        return templates.TemplateResponse(request, "mrs.html", {"state": state})

    @app.get("/vulns")
    async def vulns_page(request: Request):
        state = get_state()
        return templates.TemplateResponse(request, "vulns.html", {"state": state})

    @app.get("/pipelines")
    async def pipelines_page(request: Request):
        state = get_state()
        return templates.TemplateResponse(request, "pipelines.html", {"state": state})

    @app.get("/events", status_code=301)
    async def events_page(request: Request):
        return RedirectResponse(url="/", status_code=301)

    @app.get("/settings")
    async def settings_page(request: Request):
        state = get_state()
        return templates.TemplateResponse(request, "settings.html", {"state": state})

    # --- htmx partials --------------------------------------------------------

    @app.get("/fragment/dashboard")
    async def fragment_dashboard(request: Request):
        state = get_state()
        return templates.TemplateResponse(
            request, "dashboard_fragment.html", {"state": state}
        )

    @app.get("/fragment/mrs")
    async def fragment_mrs(request: Request, filter: str = "all"):
        state = get_state()
        mrs = (state or {}).get("mrs", [])
        if filter == "mine":
            usernames = (state or {}).get("usernames", {})
            mine_users = set(usernames.values())
            mrs = [m for m in mrs if m["author"] in mine_users]
        return templates.TemplateResponse(
            request, "mrs_fragment.html", {"mrs": mrs, "state": state, "filter": filter}
        )

    @app.get("/fragment/vulns")
    async def fragment_vulns(request: Request, filter: str = "active"):
        state = get_state()
        vulns = (state or {}).get("vulns", [])
        parts = set(filter.split(",")) if filter else set()
        if "active" in parts:
            vulns = [v for v in vulns if v.get("state", "").lower() not in ("resolved", "dismissed")]
        sev_filter = parts & {"critical", "high", "medium", "low"}
        if sev_filter:
            vulns = [v for v in vulns if v.get("severity") in sev_filter]
        return templates.TemplateResponse(
            request, "vulns_fragment.html", {"vulns": vulns, "state": state, "filter": filter}
        )

    @app.get("/fragment/pipelines")
    async def fragment_pipelines(request: Request, filter: str = "all"):
        state = get_state()
        pipelines = (state or {}).get("pipelines", [])
        if filter == "failed":
            pipelines = [p for p in pipelines if p.get("status") == "failed"]
        return templates.TemplateResponse(
            request, "pipelines_fragment.html", {"pipelines": pipelines, "state": state, "filter": filter}
        )

    @app.get("/fragment/sync-status")
    async def fragment_sync_status(request: Request):
        state = get_state()
        return templates.TemplateResponse(
            request, "sync_status_fragment.html", {"state": state}
        )

    @app.get("/fragment/events-panel")
    async def fragment_events_panel(request: Request):
        state = get_state()
        return templates.TemplateResponse(
            request, "events_panel_fragment.html", {"state": state}
        )

    # --- actions --------------------------------------------------------------

    @app.post("/action/sync")
    async def action_sync(request: Request):
        if trigger_refresh is not None:
            await trigger_refresh()
        return Response(status_code=204)

    @app.post("/action/dismiss-event/{event_id}")
    async def action_dismiss_event(event_id: int, request: Request):
        if dismiss_event:
            await dismiss_event(event_id)
        return HTMLResponse("<td colspan='5' class='text-dismissed'>dismissed</td>")

    @app.post("/action/dismiss-all-events")
    async def action_dismiss_all_events(request: Request):
        if dismiss_all_events:
            await dismiss_all_events()
        return Response(status_code=204)

    @app.post("/api/refresh")
    async def api_refresh(request: Request):
        """Trigger a refresh and wait for completion."""
        if trigger_refresh:
            await trigger_refresh()

        # Re-fetch state after refresh completes
        state = get_state()
        return {
            "mrs": len(state.get("mrs", [])),
            "pipelines": len(state.get("pipelines", [])),
            "vulns": len(state.get("vulns", [])),
            "events": len(state.get("events", [])),
        }

    # --- pin / unpin / reorder ------------------------------------------------

    @app.post("/action/pin/{repo_name}", status_code=204)
    async def action_pin(repo_name: str):
        if pin_repo:
            await pin_repo(repo_name)

    @app.post("/action/unpin/{repo_name}", status_code=204)
    async def action_unpin(repo_name: str):
        if unpin_repo:
            await unpin_repo(repo_name)

    @app.post("/action/reorder", status_code=204)
    async def action_reorder(request: Request):
        if save_repo_order:
            data = await request.json()
            await save_repo_order(data.get("order", []))

    # --- repo detail ----------------------------------------------------------

    @app.get("/repo/{name}")
    async def repo_page(name: str, request: Request):
        state = get_state()
        repo = next(
            (r for r in (state or {}).get("repos", []) if r["name"] == name), None
        )
        return templates.TemplateResponse(
            request, "repo.html", {"state": state, "repo": repo, "repo_name": name}
        )

    @app.get("/fragment/repo/{name}/mrs")
    async def fragment_repo_mrs(name: str, request: Request):
        state = get_state()
        mrs = [m for m in (state or {}).get("mrs", []) if m["repo_name"] == name]
        return templates.TemplateResponse(
            request,
            "repo_mrs_fragment.html",
            {"mrs": mrs, "state": state, "repo_name": name},
        )

    @app.get("/fragment/repo/{name}/pipelines")
    async def fragment_repo_pipelines(name: str, request: Request):
        state = get_state()
        pipelines = [
            p for p in (state or {}).get("pipelines", []) if p["repo_name"] == name
        ]
        return templates.TemplateResponse(
            request,
            "repo_pipelines_fragment.html",
            {"pipelines": pipelines, "repo_name": name},
        )

    @app.get("/fragment/repo/{name}/vulns")
    async def fragment_repo_vulns(name: str, request: Request):
        state = get_state()
        vulns = [v for v in (state or {}).get("vulns", []) if v["repo_name"] == name]
        return templates.TemplateResponse(
            request,
            "repo_vulns_fragment.html",
            {"vulns": vulns, "repo_name": name},
        )

    @app.get("/fragment/pipeline-jobs/{repo_name}/{pipeline_id}")
    async def fragment_pipeline_jobs(
        repo_name: str, pipeline_id: int, request: Request
    ):
        jobs = []
        if get_pipeline_jobs:
            jobs = await get_pipeline_jobs(repo_name, pipeline_id)
        return templates.TemplateResponse(
            request, "pipeline_jobs_fragment.html", {"jobs": jobs}
        )

    @app.post("/action/fetch/{repo_name}", status_code=204)
    async def action_fetch_repo(repo_name: str):
        if fetch_repo:
            await fetch_repo(repo_name)

    @app.post("/action/pull/{repo_name}")
    async def action_pull_repo(repo_name: str):
        if pull_repo:
            result = await pull_repo(repo_name)
            return result
        return {"error": "no pull handler"}

    # --- settings / config ----------------------------------------------------

    @app.post("/action/exclude/{repo_name}", status_code=204)
    async def action_exclude(repo_name: str):
        if exclude_repo:
            await exclude_repo(repo_name)

    @app.post("/action/include/{repo_name}", status_code=204)
    async def action_include(repo_name: str):
        if include_repo:
            await include_repo(repo_name)

    @app.post("/action/rescan", status_code=204)
    async def action_rescan():
        if rescan:
            await rescan()

    return app


def _find_free_port(start: int) -> int:
    for port in range(start, start + 100):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            if s.connect_ex(("127.0.0.1", port)) != 0:
                return port
    return start


async def start_webui(app: FastAPI, port: int) -> tuple[uvicorn.Server, int]:
    actual_port = _find_free_port(port)
    config = uvicorn.Config(
        app,
        host="127.0.0.1",
        port=actual_port,
        log_level="warning",
        log_config=None,
    )
    server = uvicorn.Server(config)
    server.install_signal_handlers = lambda: None
    asyncio.create_task(server.serve())
    return server, actual_port
