import type { Queryable } from './db.js';
import { MusicBrainzClient } from './integrations/musicbrainz.client.js';
import { normalizeForMatching } from './utils/fuzzy-match.js';

function generateSortName(name: string): string {
  const match = name.match(/^(The|A|An)\s+(.+)$/i);
  if (match) return `${match[2]}, ${match[1]}`;
  return name;
}

export class MbSpineWriter {
  constructor(private pgClient: Queryable) {}

  /**
   * Fetch full artist-credit for a recording MBID and write claims.
   * - First credit → "credited_main_on"
   * - Additional credits → "featured_on"
   * - Updates tracks.recording_mbid
   * - Resolves or stubs credited artists by MBID
   */
  async writeRecordingClaims(
    recordingMbid: string,
    trackId: string,
    mbClient: MusicBrainzClient
  ): Promise<number> {
    const recording = await mbClient.getRecording(recordingMbid);
    if (!recording || recording.artistCredit.length === 0) return 0;

    await this.pgClient.query(
      `UPDATE tracks SET recording_mbid = $1 WHERE id = $2 AND recording_mbid IS DISTINCT FROM $1`,
      [recordingMbid, trackId]
    );

    let claimsWritten = 0;

    for (let i = 0; i < recording.artistCredit.length; i++) {
      const credit = recording.artistCredit[i];

      const artistId = await this.resolveArtist(
        credit.artistMbid,
        credit.artistName,
        credit.creditName,
      );
      if (!artistId) {
        console.warn(
          `[MbSpineWriter] Could not resolve artist for credit ${i} on recording ${recordingMbid}`
        );
        continue;
      }

      const predicate = i === 0 ? 'credited_main_on' : 'featured_on';
      const raw = credit.joinphrase ? JSON.stringify({ joinphrase: credit.joinphrase }) : null;

      await this.pgClient.query(
        `INSERT INTO claims
           (subject_type, subject_id, predicate, object_type, object_id, source, confidence, raw)
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
         ON CONFLICT (subject_type, subject_id, predicate, object_type, object_id, source, user_id)
         DO NOTHING`,
        ['track', trackId, predicate, 'artist', artistId, 'mb', 1.0, raw]
      );
      claimsWritten++;
    }

    return claimsWritten;
  }

  /**
   * Fetch full artist-credit for a release-group MBID and write claims.
   * - First credit → "credited_main_on_album"
   * - Additional credits → "featured_on_album"
   * - Resolves or stubs credited artists by MBID
   */
  async writeAlbumClaims(
    releaseGroupMbid: string,
    albumId: string,
    mbClient: MusicBrainzClient
  ): Promise<number> {
    const releaseGroup = await mbClient.getReleaseGroup(releaseGroupMbid);
    if (!releaseGroup || releaseGroup.artistCredit.length === 0) return 0;

    let claimsWritten = 0;

    for (let i = 0; i < releaseGroup.artistCredit.length; i++) {
      const credit = releaseGroup.artistCredit[i];

      const artistId = await this.resolveArtist(
        credit.artistMbid,
        credit.artistName,
        credit.creditName,
      );
      if (!artistId) {
        console.warn(
          `[MbSpineWriter] Could not resolve artist for credit ${i} on release-group ${releaseGroupMbid}`
        );
        continue;
      }

      const predicate = i === 0 ? 'credited_main_on_album' : 'featured_on_album';
      const raw = credit.joinphrase ? JSON.stringify({ joinphrase: credit.joinphrase }) : null;

      await this.pgClient.query(
        `INSERT INTO claims
           (subject_type, subject_id, predicate, object_type, object_id, source, confidence, raw)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        ON CONFLICT (subject_type, subject_id, predicate, object_type, object_id, source, user_id)
        DO NOTHING`,
        ['artist', artistId, predicate, 'album', albumId, 'mb', 1.0, raw]
      );
      claimsWritten++;
    }

    return claimsWritten;
  }

  /**
   * Fetch an artist's MB artist-relations and write structural claims:
   * - "member of" → "member_of" (subject = this artist, object = target group)
   * - "is alias of" / "is performance name of" / "is legal name of" → "alias_of"
   *
   * Direction handling: MB marks the relationship as forward when it points from
   * the queried artist to the target. For "is performance name of" (forward)
   * the queried artist is the alias of the target, so subject = queried artist,
   * object = target. For backward relations we flip subject/object.
   *
   * Collab/vocal/instrument ARs are artist-artist contributions rather than
   * identity/structure, and the spec maps them to track-level featured_on; we
   * skip them here to avoid over-claiming artist-artist edges.
   */
  async writeArtistRelationClaims(
    artistMbid: string,
    artistId: string,
    mbClient: MusicBrainzClient
  ): Promise<number> {
    const relations = await mbClient.getArtistRelations(artistMbid);
    if (relations.length === 0) return 0;

    const memberOfTypes = new Set(['member of', 'founder of', 'founder']);
    const aliasOfTypes = new Set(['is alias of', 'is performance name of', 'is legal name of']);

    let claimsWritten = 0;

    for (const rel of relations) {
      const isMemberOf = [...memberOfTypes].some(t => rel.type.toLowerCase() === t);
      const aliasMatch = [...aliasOfTypes].find(t => rel.type.toLowerCase() === t);
      if (!isMemberOf && !aliasMatch) continue;

      const targetArtistId = await this.resolveArtist(
        rel.targetMbid,
        rel.targetName,
        rel.targetName,
      );
      if (!targetArtistId) {
        console.warn(
          `[MbSpineWriter] Could not resolve target artist for relation "${rel.type}" on artist ${artistMbid}`
        );
        continue;
      }

      // Forward direction: subject = this artist, object = target.
      // Backward direction: the relation is phrased from target → subject, so
      // we flip subject/object to preserve the predicate's intended direction.
      let subjectId = artistId;
      let objectId = targetArtistId;
      if (rel.direction === 'backward') {
        subjectId = targetArtistId;
        objectId = artistId;
      }

      const predicate = aliasMatch ? 'alias_of' : 'member_of';
      const raw = JSON.stringify({
        mb_relation_type: rel.type,
        direction: rel.direction,
      });

      await this.pgClient.query(
        `INSERT INTO claims
           (subject_type, subject_id, predicate, object_type, object_id, source, confidence, raw)
        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
        ON CONFLICT (subject_type, subject_id, predicate, object_type, object_id, source, user_id)
        DO NOTHING`,
        ['artist', subjectId, predicate, 'artist', objectId, 'mb', 1.0, raw]
      );
      claimsWritten++;
    }

    return claimsWritten;
  }

  /**
   * Resolve an MB artist to a local artists.id. Priority:
   * 1. Match by mbid (UUID column)
   * 2. Match by normalized credit name
   * 3. Create a stub row
   */
  private async resolveArtist(
    mbid: string,
    artistName: string,
    creditName: string,
  ): Promise<string | null> {
    const existing = await this.pgClient.query<{ id: string }>(
      `SELECT id FROM artists WHERE mbid = $1`,
      [mbid]
    );
    if (existing.rows.length > 0) return existing.rows[0].id;

    const normalized = normalizeForMatching(creditName);
    const nameMatch = await this.pgClient.query<{ id: string }>(
      `SELECT id FROM artists WHERE normalized_name = $1`,
      [normalized]
    );
    if (nameMatch.rows.length > 0) {
      await this.pgClient.query(
        `UPDATE artists SET mbid = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2`,
        [mbid, nameMatch.rows[0].id]
      );
      return nameMatch.rows[0].id;
    }

    const sortName = generateSortName(artistName);
    const result = await this.pgClient.query<{ id: string }>(
      `INSERT INTO artists (name, canonical_name, sort_name, mbid)
       VALUES ($1, $2, $3, $4)
       ON CONFLICT (mbid) WHERE mbid IS NOT NULL
       DO UPDATE SET name = EXCLUDED.name, updated_at = CURRENT_TIMESTAMP
       RETURNING id`,
      [creditName, artistName, sortName, mbid]
    );
    return result.rows[0]?.id ?? null;
  }
}
