//! Generates a project-level CLAUDE.md that describes the current state of the project
//! for AI assistants. This file is regenerated every time entities, auth, or admin are modified,
//! so AI agents always have up-to-date knowledge of the schema, relations, and API endpoints.

use crate::{relation, utils};
use anyhow::Result;
use heck::ToPascalCase;
use std::fs;
use std::path::Path;

/// Regenerate the project-level CLAUDE.md based on current project state.
/// Called after scaffold, entity generation, auth generation, and admin generation.
pub fn regenerate(project_root: &Path) -> Result<()> {
    let mut sections = Vec::new();

    sections.push(generate_header(project_root));
    sections.push(generate_stack_section());
    sections.push(generate_cli_reference_section());

    let entities = scan_entities(project_root)?;
    if !entities.is_empty() {
        sections.push(generate_entity_section(&entities));
        sections.push(generate_relations_section(project_root, &entities)?);
        sections.push(generate_api_section(&entities));
    }

    if has_auth(project_root) {
        sections.push(generate_auth_section());
    }

    if has_admin(project_root) {
        sections.push(generate_admin_section(&entities));
    }

    let addons = scan_addons(project_root);
    if !addons.is_empty() {
        sections.push(generate_addons_section(&addons));
    }

    sections.push(generate_conventions_section());

    let generated = sections.join("\n\n");

    // Preserve custom notes if file already exists
    let claude_path = project_root.join("CLAUDE.md");
    let custom_marker = "\n## Custom Notes";
    let content = if claude_path.exists() {
        let existing = fs::read_to_string(&claude_path)?;
        if let Some(pos) = existing.rfind(custom_marker) {
            let custom_block = &existing[pos..];
            // Replace everything before Custom Notes with new generated content
            // Strip the default Custom Notes from generated
            if let Some(gen_pos) = generated.rfind(custom_marker) {
                format!("{}\n{}", &generated[..gen_pos].trim_end(), custom_block)
            } else {
                format!("{}\n{}", generated.trim_end(), custom_block)
            }
        } else {
            generated
        }
    } else {
        generated
    };

    fs::write(&claude_path, &content)?;
    println!("  Updated CLAUDE.md (AI context)");

    Ok(())
}

fn generate_header(project_root: &Path) -> String {
    let project_name = project_root
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "project".to_string());

    format!(
        r#"# {} — Project Guide

> Auto-generated by Romance CLI. Do not edit the sections above `## Custom Notes` — they are regenerated when entities or features change.

## Project Overview

Full-stack application built with **Romance CLI** scaffold."#,
        project_name
    )
}

fn generate_stack_section() -> String {
    r#"## Stack

- **Backend:** Rust (Axum, SeaORM, PostgreSQL)
- **Frontend:** React 19, TypeScript, TanStack Query, shadcn/ui, Vite
- **API pattern:** REST with JSON envelope `{success, data?, meta?, error?}`
- **Auth:** JWT (jsonwebtoken + argon2) — Bearer token in Authorization header
- **DB:** PostgreSQL with SeaORM migrations"#
        .to_string()
}

fn generate_cli_reference_section() -> String {
    r#"## Romance CLI Reference

### Commands

| Command | Description | Example |
|---------|-------------|---------|
| `romance new <name>` | Create new full-stack project | `romance new my-app` |
| `romance generate entity <name> [fields...]` | Generate CRUD entity | `romance generate entity Product title:string price:decimal` |
| `romance generate auth` | Add JWT authentication | `romance generate auth` |
| `romance generate admin` | Add admin panel (requires auth) | `romance generate admin` |
| `romance add <addon>` | Install addon | `romance add validation` |
| `romance dev` | Run backend + frontend dev servers | `romance dev` |
| `romance db migrate` | Run pending migrations | `romance db migrate` |
| `romance db rollback` | Rollback last migration | `romance db rollback` |
| `romance db seed` | Run seed data | `romance db seed` |
| `romance test` | Run tests with temp database | `romance test` |
| `romance check` | Run cargo check + tsc | `romance check` |
| `romance update` | Update scaffold to latest templates | `romance update` |

### Field Syntax

- `name:type` — required field: `title:string`
- `name:type?` — optional field: `description:text?`
- `name:type->Entity` — foreign key: `author_id:uuid->User`
- `name:has_many->Entity` — has-many relation: `posts:has_many->Post`
- `name:m2m->Entity` — many-to-many: `tags:m2m->Tag`

### Field Types

| Type | Aliases | Rust | TypeScript |
|------|---------|------|------------|
| `string` | `str` | `String` | `string` |
| `text` | — | `String` | `string` |
| `bool` | `boolean` | `bool` | `boolean` |
| `int` | `i32`, `int32`, `integer` | `i32` | `number` |
| `bigint` | `i64`, `int64` | `i64` | `number` |
| `float` | `f64`, `float64`, `double` | `f64` | `number` |
| `decimal` | `money` | `Decimal` | `number` |
| `uuid` | — | `Uuid` | `string` |
| `datetime` | `timestamp` | `DateTimeWithTimeZone` | `string` |
| `date` | — | `Date` | `string` |
| `json` | `jsonb` | `Json` | `unknown` |
| `file` | — | `String` (URL) | `string` |
| `image` | — | `String` (URL) | `string` |

### Validation Syntax

Append `[rules]` to field type: `name:string[min=3,max=100]`, `email:string[email]`, `title:string[searchable]`

Available rules: `min=N`, `max=N`, `email`, `url`, `searchable`

### Available Addons

| Addon | Command | What it adds |
|-------|---------|-------------|
| validation | `romance add validation` | Backend `validator` + frontend Zod schemas |
| security | `romance add security` | Rate limiting + security headers |
| observability | `romance add observability` | Structured tracing + request ID |
| storage | `romance add storage` | File/image upload endpoints |
| soft-delete | `romance add soft-delete` | Soft delete with restore |
| audit-log | `romance add audit-log` | CUD operation logging (requires auth) |
| search | `romance add search` | PostgreSQL full-text search |
| oauth | `romance add oauth <provider>` | Social auth (google, github, discord) |
| dashboard | `romance add dashboard` | Dev dashboard at /dev |"#
        .to_string()
}

/// Scan entity model files to extract field information.
fn scan_entities(project_root: &Path) -> Result<Vec<EntityInfo>> {
    let entities_dir = project_root.join("backend/src/entities");
    if !entities_dir.exists() {
        return Ok(Vec::new());
    }

    let entity_names = relation::discover_entities(project_root)?;
    let mut entities = Vec::new();

    for name in &entity_names {
        // Skip junction tables (contain underscore and both halves are entity names)
        let model_path = entities_dir.join(format!("{}.rs", name));
        if !model_path.exists() {
            continue;
        }

        let content = fs::read_to_string(&model_path)?;

        // Skip if it looks like a junction table (no ROMANCE:CUSTOM marker)
        if !content.contains("ROMANCE:CUSTOM") && !content.contains("ROMANCE:RELATIONS") {
            // It's probably a junction — include it separately
            continue;
        }

        let fields = parse_model_fields(&content);
        let pascal_name = name.to_pascal_case();

        entities.push(EntityInfo {
            name: pascal_name,
            snake_name: name.clone(),
            fields,
        });
    }

    Ok(entities)
}

/// Parse field names and types from a SeaORM model file.
fn parse_model_fields(content: &str) -> Vec<FieldInfo> {
    let mut fields = Vec::new();
    let mut in_model = false;

    for line in content.lines() {
        let trimmed = line.trim();

        // Detect start of Model struct
        if trimmed.contains("pub struct Model") {
            in_model = true;
            continue;
        }

        // End of struct
        if in_model && trimmed == "}" {
            break;
        }

        if in_model && trimmed.starts_with("pub ") {
            // Parse: pub field_name: Type,
            let rest = trimmed.strip_prefix("pub ").unwrap_or(trimmed);
            if let Some((name, type_part)) = rest.split_once(':') {
                let name = name.trim().to_string();
                let rust_type = type_part.trim().trim_end_matches(',').to_string();

                // Skip id, created_at, updated_at — they're standard
                if name == "id" || name == "created_at" || name == "updated_at" {
                    continue;
                }

                let optional = rust_type.starts_with("Option<");
                let clean_type = if optional {
                    rust_type
                        .strip_prefix("Option<")
                        .and_then(|s| s.strip_suffix('>'))
                        .unwrap_or(&rust_type)
                        .to_string()
                } else {
                    rust_type.clone()
                };

                let is_fk = name.ends_with("_id");

                fields.push(FieldInfo {
                    name,
                    rust_type: clean_type,
                    optional,
                    is_fk,
                });
            }
        }
    }

    fields
}

fn generate_entity_section(entities: &[EntityInfo]) -> String {
    let mut s = String::from("## Database Schema\n\n");

    for entity in entities {
        s.push_str(&format!("### {} (`{}` table)\n\n", entity.name, utils::pluralize(&entity.snake_name)));
        s.push_str("| Column | Type | Nullable | Notes |\n");
        s.push_str("|--------|------|----------|-------|\n");
        s.push_str("| `id` | UUID | no | Primary key |\n");

        for field in &entity.fields {
            let nullable = if field.optional { "yes" } else { "no" };
            let notes = if field.is_fk {
                let target = field.name.strip_suffix("_id").unwrap_or(&field.name);
                format!("FK → `{}.id`", utils::pluralize(target))
            } else {
                String::new()
            };
            s.push_str(&format!(
                "| `{}` | {} | {} | {} |\n",
                field.name, field.rust_type, nullable, notes
            ));
        }

        s.push_str("| `created_at` | DateTimeWithTimeZone | no | |\n");
        s.push_str("| `updated_at` | DateTimeWithTimeZone | no | |\n");
        s.push('\n');
    }

    s
}

fn generate_relations_section(project_root: &Path, entities: &[EntityInfo]) -> Result<String> {
    let mut s = String::from("## Relations\n\n");
    let mut has_any = false;

    let entities_dir = project_root.join("backend/src/entities");

    for entity in entities {
        let model_path = entities_dir.join(format!("{}.rs", entity.snake_name));
        if !model_path.exists() {
            continue;
        }
        let content = fs::read_to_string(&model_path)?;

        // Parse belongs_to from Relation enum
        for line in content.lines() {
            let trimmed = line.trim();
            if trimmed.starts_with("belongs_to = \"") {
                // Extract: belongs_to = "super::user::Entity"
                if let Some(target) = trimmed
                    .strip_prefix("belongs_to = \"super::")
                    .and_then(|s| s.strip_suffix("::Entity\","))
                    .or_else(|| {
                        trimmed
                            .strip_prefix("belongs_to = \"super::")
                            .and_then(|s| s.strip_suffix("::Entity\""))
                    })
                {
                    s.push_str(&format!(
                        "- **{} → {}** (belongs_to)\n",
                        entity.name,
                        target.to_pascal_case()
                    ));
                    has_any = true;
                }
            }
        }

        // Parse M2M via Related impls that contain fn via()
        // Only Related impls with via() are M2M; others are belongs_to
        {
            let mut in_related_impl = false;
            let mut related_target: Option<String> = None;
            let mut has_via = false;
            let mut brace_depth = 0;

            for line in content.lines() {
                let trimmed = line.trim();

                if !in_related_impl
                    && trimmed.starts_with("impl Related<super::")
                    && trimmed.contains("::Entity>")
                {
                    related_target = trimmed
                        .strip_prefix("impl Related<super::")
                        .and_then(|s| s.split("::Entity>").next())
                        .map(|s| s.to_string());
                    in_related_impl = true;
                    has_via = false;
                    brace_depth = 0;
                }

                if in_related_impl {
                    brace_depth += trimmed.matches('{').count();
                    brace_depth -= trimmed.matches('}').count();

                    if trimmed.contains("fn via()") {
                        has_via = true;
                    }

                    if brace_depth == 0 {
                        if has_via {
                            if let Some(ref target) = related_target {
                                let a = &entity.name;
                                let b = target.to_pascal_case();
                                // Deduplicate: only emit if a < b alphabetically
                                if a.as_str() <= b.as_str() {
                                    s.push_str(&format!(
                                        "- **{} ↔ {}** (many-to-many)\n",
                                        a, b
                                    ));
                                    has_any = true;
                                }
                            }
                        }
                        in_related_impl = false;
                        related_target = None;
                    }
                }
            }
        }
    }

    // Scan for junction tables
    let junction_entities = relation::discover_entities(project_root)?;
    for name in &junction_entities {
        if name.contains('_') {
            let model_path = entities_dir.join(format!("{}.rs", name));
            if model_path.exists() {
                let content = fs::read_to_string(&model_path)?;
                if !content.contains("ROMANCE:CUSTOM") {
                    s.push_str(&format!("- Junction table: `{}`\n", name));
                    has_any = true;
                }
            }
        }
    }

    if !has_any {
        s.push_str("No relations defined yet.\n");
    }

    Ok(s)
}

fn generate_api_section(entities: &[EntityInfo]) -> String {
    let mut s = String::from("## API Endpoints\n\n");
    s.push_str("All responses use the envelope: `{success: bool, data?: T, meta?: {page?}, error?: {code, message}}`\n\n");

    for entity in entities {
        let plural = utils::pluralize(&entity.snake_name);
        s.push_str(&format!("### {} CRUD\n\n", entity.name));
        s.push_str(&format!("| Method | Path | Description |\n"));
        s.push_str("|--------|------|-------------|\n");
        s.push_str(&format!(
            "| GET | `/api/{}?page=1&per_page=20` | List (paginated) |\n",
            plural
        ));
        s.push_str(&format!(
            "| POST | `/api/{}` | Create |\n",
            plural
        ));
        s.push_str(&format!(
            "| GET | `/api/{}/:id` | Get by ID |\n",
            plural
        ));
        s.push_str(&format!(
            "| PUT | `/api/{}/:id` | Update |\n",
            plural
        ));
        s.push_str(&format!(
            "| DELETE | `/api/{}/:id` | Delete |\n",
            plural
        ));

        // Check for FK fields → has-many sub-routes exist on the target
        for field in &entity.fields {
            if field.is_fk {
                let _target = field.name.strip_suffix("_id").unwrap_or(&field.name);
                // The parent entity has a sub-route for this entity's items
                // We'll document this on the parent's side
            }
        }

        s.push('\n');
    }

    s
}

fn has_auth(project_root: &Path) -> bool {
    project_root.join("backend/src/auth.rs").exists()
}

fn has_admin(project_root: &Path) -> bool {
    project_root.join("backend/src/handlers/admin.rs").exists()
}

fn generate_auth_section() -> String {
    r#"## Authentication

JWT-based auth with argon2 password hashing.

| Method | Path | Description | Auth required |
|--------|------|-------------|---------------|
| POST | `/api/auth/register` | Register new user | No |
| POST | `/api/auth/login` | Login, returns JWT | No |
| GET | `/api/auth/me` | Get current user | Yes |

**Token usage:** Include `Authorization: Bearer <token>` header. Token is stored in `localStorage` under key `auth_token`.

**User roles:** `user` (default), `admin`

**Axum extractors:**
- `AuthUser` — requires valid JWT, any role
- `AdminUser` — requires valid JWT with `admin` role"#
        .to_string()
}

fn generate_admin_section(entities: &[EntityInfo]) -> String {
    let mut s = String::from("## Admin Panel\n\n");
    s.push_str("Admin routes require `admin` role JWT. All entity CRUD is mirrored under `/api/admin/`.\n\n");
    s.push_str("| Method | Path | Description |\n");
    s.push_str("|--------|------|-------------|\n");
    s.push_str("| GET | `/api/admin/dashboard` | Dashboard stats (entity counts) |\n");

    for entity in entities {
        s.push_str(&format!(
            "| GET/POST/PUT/DELETE | `/api/admin/{}/*` | Admin CRUD for {} |\n",
            utils::pluralize(&entity.snake_name), entity.name
        ));
    }

    s.push_str("\n**Frontend:** `/admin` route with sidebar navigation, dashboard, and entity management pages.\n");
    s
}

fn generate_conventions_section() -> String {
    r#"## Conventions

- **Entity files:** `backend/src/entities/{snake}.rs`, `handlers/{snake}.rs`, `routes/{snake}.rs`
- **Frontend features:** `frontend/src/features/{camelCase}/` with types.ts, api.ts, hooks.ts, List/Form/Detail components
- **Migrations:** `backend/migration/src/m{timestamp}_create_{snake}_table.rs`
- **Markers:** `// === ROMANCE:TYPE ===` comments are insertion points — do not remove them
- **Custom code:** Everything below `// === ROMANCE:CUSTOM ===` is preserved on re-generation
- **API envelope:** All responses wrapped in `{success, data?, meta?, error?}`
- **Pagination:** `?page=1&per_page=20` query params, response includes `meta.page` with `total`, `total_pages`, `has_next`, `has_prev`

## Custom Notes

<!-- Add your project-specific notes below this line. This section is preserved on re-generation. -->"#
        .to_string()
}

fn scan_addons(project_root: &Path) -> Vec<String> {
    let mut addons = Vec::new();

    if project_root.join("backend/src/validation.rs").exists() {
        addons.push("validation".to_string());
    }
    if project_root.join("backend/src/middleware/security_headers.rs").exists() {
        addons.push("security".to_string());
    }
    if project_root.join("backend/src/middleware/request_id.rs").exists() {
        addons.push("observability".to_string());
    }
    if project_root.join("backend/src/storage.rs").exists() {
        addons.push("storage".to_string());
    }
    if project_root.join("backend/src/soft_delete.rs").exists() {
        addons.push("soft-delete".to_string());
    }
    if project_root.join("backend/src/audit.rs").exists() {
        addons.push("audit-log".to_string());
    }
    if project_root.join("backend/src/oauth.rs").exists() {
        addons.push("oauth".to_string());
    }
    if project_root.join("backend/src/search.rs").exists() {
        addons.push("search".to_string());
    }
    if project_root.join("frontend/src/features/dev/DevDashboard.tsx").exists() {
        addons.push("dashboard".to_string());
    }
    if project_root.join("backend/src/tenant.rs").exists() {
        addons.push("multitenancy".to_string());
    }

    addons
}

fn generate_addons_section(addons: &[String]) -> String {
    let mut s = String::from("## Installed Addons\n\n");

    for addon in addons {
        match addon.as_str() {
            "validation" => {
                s.push_str("### Validation\n");
                s.push_str("- Backend: `validator` crate with `#[validate]` derives and `ValidatedJson<T>` extractor\n");
                s.push_str("- Frontend: Zod schemas generated alongside types\n");
                s.push_str("- Field syntax: `name:string[min=3,max=100]`, `email:string[email]`\n\n");
            }
            "security" => {
                s.push_str("### Security Middleware\n");
                s.push_str("- Security headers (X-Content-Type-Options, X-Frame-Options, HSTS, CSP)\n");
                s.push_str("- Rate limiting (configurable in romance.toml)\n");
                s.push_str("- CORS (configurable origins)\n\n");
            }
            "observability" => {
                s.push_str("### Observability\n");
                s.push_str("- Structured JSON logging with `tracing`\n");
                s.push_str("- Request ID propagation (X-Request-Id header)\n");
                s.push_str("- Configure via RUST_LOG env var\n\n");
            }
            "storage" => {
                s.push_str("### File Storage\n");
                s.push_str("- Upload endpoint: `POST /api/upload`, `POST /api/upload/image`\n");
                s.push_str("- Local storage backend (S3 supported via config)\n");
                s.push_str("- Field types: `file`, `image` (stored as URL string in DB)\n\n");
            }
            "soft-delete" => {
                s.push_str("### Soft Delete\n");
                s.push_str("- Entities have `deleted_at` column (NULL = not deleted)\n");
                s.push_str("- DELETE sets `deleted_at` (soft delete)\n");
                s.push_str("- `POST /:id/restore` to restore\n");
                s.push_str("- `DELETE /:id/permanent` for hard delete\n\n");
            }
            "audit-log" => {
                s.push_str("### Audit Log\n");
                s.push_str("- All CUD operations logged to `audit_entries` table\n");
                s.push_str("- Fields: entity_type, entity_id, action, user_id, changes (JSONB)\n");
                s.push_str("- Admin viewer at `/admin/audit-log`\n\n");
            }
            "oauth" => {
                s.push_str("### OAuth\n");
                s.push_str("- Social auth flow: `GET /api/auth/oauth/{provider}` → redirect → callback\n");
                s.push_str("- Users table has `oauth_provider` and `oauth_id` columns\n");
                s.push_str("- Configure client ID/secret in `.env`\n\n");
            }
            "search" => {
                s.push_str("### Full-Text Search\n");
                s.push_str("- PostgreSQL-based search with ILIKE/tsvector\n");
                s.push_str("- Field syntax: `title:string[searchable]`\n");
                s.push_str("- Search endpoint: `GET /api/{entities}/search?q=term`\n\n");
            }
            "dashboard" => {
                s.push_str("### Dev Dashboard\n");
                s.push_str("- Developer dashboard at `/dev`\n");
                s.push_str("- Entity counts, API endpoint reference\n\n");
            }
            "multitenancy" => {
                s.push_str("### Multitenancy\n");
                s.push_str("- Row-level tenant isolation: all entities get `tenant_id` column\n");
                s.push_str("- `TenantGuard` extractor reads tenant_id from JWT claims\n");
                s.push_str("- All CRUD queries automatically filter by tenant_id\n");
                s.push_str("- Tenant admin API: `POST/GET /api/tenants` (admin-only)\n");
                s.push_str("- Users belong to a tenant (`users.tenant_id` FK)\n\n");
            }
            _ => {}
        }
    }

    s
}

// Internal data structures for scanning

struct EntityInfo {
    name: String,
    snake_name: String,
    fields: Vec<FieldInfo>,
}

struct FieldInfo {
    name: String,
    rust_type: String,
    optional: bool,
    is_fk: bool,
}
