import io
import json
import shutil
import zipfile
from pathlib import Path
from tempfile import TemporaryDirectory

from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import Response

from c2m_by_nery.core.config import ConfigError, ConfigManager
from c2m_by_nery.core.pipeline import run_conversion, run_single_file_conversion

app = FastAPI(
    title="c2m-by-nery API",
    description="Convert code and documents to Markdown. Upload a .zip (repository or folder) or a single file (e.g. .log, .pdf, .md). Returns a .zip of .md output.",
    version="0.2.0",
)


def _parse_bool(value: str | None) -> bool | None:
    if value is None:
        return None
    v = value.strip().lower()
    if v in ("true", "1", "yes"):
        return True
    if v in ("false", "0", "no"):
        return False
    return None


def _parse_ignore_patterns(value: str | None) -> list[str] | None:
    if value is None or not value.strip():
        return None
    try:
        data = json.loads(value)
        if not isinstance(data, list):
            return None
        return [str(x).strip() for x in data if isinstance(x, str)]
    except json.JSONDecodeError:
        return None


@app.post(
    "/convert",
    response_class=Response,
    summary="Convert to Markdown",
    description="Upload a **.zip** (repository or folder) or a **single file** (e.g. .log, .pdf, .md, .docx). Returns a .zip of Markdown files (and report.json). Optional form fields override conversion options; leave them empty to use defaults.",
)
async def convert(
    file: UploadFile = File(
        ...,
        description="Required. A .zip file (repo or folder) or a single file (e.g. .log, .pdf, .md, .docx). Both are accepted.",
    ),
    token_limit: int | None = Form(
        default=None,
        description="Token limit per output segment. Leave empty for config default (e.g. 32768). Must be positive if set.",
    ),
    flat: bool = Form(
        False,
        description="Output all .md in one folder (no subfolders).",
    ),
    single_file: bool = Form(
        False,
        description="Merge all content into one output.md.",
    ),
    frontmatter: bool = Form(
        True,
        description="Include YAML frontmatter in output.",
    ),
    ignore_patterns: str | None = Form(
        default=None,
        description='Optional JSON array of ignore patterns, e.g. ["**/dist/**"]. Leave empty for none.',
    ),
) -> Response:
    if not file.filename:
        raise HTTPException(status_code=400, detail="File is required.")

    flat_bool = flat
    single_file_bool = single_file
    frontmatter_bool = frontmatter

    contents = await file.read()
    if not contents:
        raise HTTPException(status_code=400, detail="Empty file.")

    is_zip = file.filename.lower().endswith(".zip")

    if not is_zip:
        with TemporaryDirectory(prefix="c2m_") as tmpdir:
            tmp = Path(tmpdir)
            single_path = tmp / (file.filename or "upload")
            single_path.write_bytes(contents)
            try:
                config = ConfigManager.load(tmp)
            except ConfigError as e:
                raise HTTPException(status_code=422, detail=f"Configuration error: {e}") from e
            token_limit_val = config.token_limit
            if token_limit is not None:
                try:
                    tl = int(token_limit)
                except (TypeError, ValueError):
                    raise HTTPException(status_code=400, detail="token_limit must be an integer")
                if tl > 0:
                    token_limit_val = tl
            if ignore_patterns_override := _parse_ignore_patterns(ignore_patterns):
                config = ConfigManager(
                    token_limit=config.token_limit,
                    output_path=config.output_path,
                    ignore_patterns=config.ignore_patterns + ignore_patterns_override,
                    flat_output=config.flat_output,
                    single_file=config.single_file,
                )
            if flat_bool is not None:
                config = ConfigManager(
                    token_limit=config.token_limit,
                    output_path=config.output_path,
                    ignore_patterns=config.ignore_patterns,
                    flat_output=flat_bool,
                    single_file=config.single_file,
                )
            if single_file_bool is not None:
                config = ConfigManager(
                    token_limit=config.token_limit,
                    output_path=config.output_path,
                    ignore_patterns=config.ignore_patterns,
                    flat_output=config.flat_output,
                    single_file=single_file_bool,
                )
            frontmatter_val = frontmatter_bool if frontmatter_bool is not None else True
            try:
                report = run_single_file_conversion(
                    single_path,
                    config,
                    token_limit_val,
                    quiet=True,
                    zip_output=True,
                    flat_output=config.flat_output,
                    single_file=config.single_file,
                    frontmatter=frontmatter_val,
                )
            except Exception as e:
                raise HTTPException(status_code=500, detail=f"Conversion failed: {e}") from e
            zip_path_str = report.get("zip_path")
            if not zip_path_str:
                raise HTTPException(status_code=500, detail="Conversion did not produce a zip")
            zip_path = Path(zip_path_str)
            if not zip_path.exists():
                raise HTTPException(status_code=500, detail="Output zip not found")
            zip_bytes = zip_path.read_bytes()
            return Response(
                content=zip_bytes,
                media_type="application/zip",
                headers={"Content-Disposition": f'attachment; filename="{report["timestamp"]}.zip"'},
            )

    try:
        with zipfile.ZipFile(io.BytesIO(contents), "r") as zf:
            pass
    except zipfile.BadZipFile:
        raise HTTPException(status_code=400, detail="Invalid zip file.")

    upload_stem = Path(file.filename or "upload").stem

    with TemporaryDirectory(prefix="c2m_") as tmpdir:
        tmp = Path(tmpdir)
        try:
            with zipfile.ZipFile(io.BytesIO(contents), "r") as zf:
                zf.extractall(tmp)
        except zipfile.BadZipFile:
            raise HTTPException(status_code=400, detail="Invalid zip file.")

        entries = [p for p in tmp.iterdir()]
        if not entries:
            raise HTTPException(status_code=400, detail="Zip is empty.")

        is_single_file = len(entries) == 1 and entries[0].is_file()
        is_single_dir = len(entries) == 1 and entries[0].is_dir()

        if is_single_file:
            root_path = entries[0]
            config_root = root_path.parent
        elif is_single_dir:
            root_path = entries[0]
            config_root = root_path
        else:
            wrapper = tmp / upload_stem
            wrapper.mkdir(parents=True, exist_ok=True)
            for p in entries:
                shutil.move(str(p), str(wrapper / p.name))
            root_path = wrapper
            config_root = root_path

        try:
            config = ConfigManager.load(config_root)
        except ConfigError as e:
            raise HTTPException(status_code=422, detail=f"Configuration error: {e}") from e

        token_limit_val = config.token_limit
        if token_limit is not None:
            try:
                tl = int(token_limit)
            except (TypeError, ValueError):
                raise HTTPException(status_code=400, detail="token_limit must be an integer")
            if tl <= 0:
                pass
            else:
                token_limit_val = tl

        ignore_patterns_override = _parse_ignore_patterns(ignore_patterns)
        if ignore_patterns_override is not None:
            config = ConfigManager(
                token_limit=config.token_limit,
                output_path=config.output_path,
                ignore_patterns=config.ignore_patterns + ignore_patterns_override,
                flat_output=config.flat_output,
                single_file=config.single_file,
            )

        if flat_bool is not None:
            config = ConfigManager(
                token_limit=config.token_limit,
                output_path=config.output_path,
                ignore_patterns=config.ignore_patterns,
                flat_output=flat_bool,
                single_file=config.single_file,
            )
        if single_file_bool is not None:
            config = ConfigManager(
                token_limit=config.token_limit,
                output_path=config.output_path,
                ignore_patterns=config.ignore_patterns,
                flat_output=config.flat_output,
                single_file=single_file_bool,
            )

        frontmatter_val = frontmatter_bool if frontmatter_bool is not None else True

        try:
            if is_single_file:
                report = run_single_file_conversion(
                    root_path,
                    config,
                    token_limit_val,
                    quiet=True,
                    zip_output=True,
                    flat_output=config.flat_output,
                    single_file=config.single_file,
                    frontmatter=frontmatter_val,
                )
            else:
                report = run_conversion(
                    root_path,
                    config,
                    token_limit_val,
                    quiet=True,
                    zip_output=True,
                    flat_output=config.flat_output,
                    single_file=config.single_file,
                    frontmatter=frontmatter_val,
                )
        except Exception as e:
            raise HTTPException(status_code=500, detail=f"Conversion failed: {e}") from e

        zip_path_str = report.get("zip_path")
        if not zip_path_str:
            raise HTTPException(status_code=500, detail="Conversion did not produce a zip")

        zip_path = Path(zip_path_str)
        if not zip_path.exists():
            raise HTTPException(status_code=500, detail="Output zip not found")

        zip_bytes = zip_path.read_bytes()
        filename = f"{report['timestamp']}.zip"
        return Response(
            content=zip_bytes,
            media_type="application/zip",
            headers={"Content-Disposition": f'attachment; filename="{filename}"'},
        )
